Redraw CustomPaint without updating state / rebuilding widget? - flutter

I'm trying to understand how customPaint works, I want to draw a custom frame by frame animation on a canvas.
I can make it work by redrawing the widget every 1/60 seconds, but that doesn't sound very efficient. I would like render the CustomPainter every 1/60 seconds but that doesn't seem to work. Any note or remark very appreciated to help me understand how I'm supposed to achieve this. Thanks.
This is the kind of code I'm working with :
class CustomAnimatedWidgetState extends State<CustomAnimatedWidget> {
CustomPaint _paint=null;
MyCustomPainter _painter=null;
double animationFrame=0;
void tick() {
//called eery 1/60 seconds
animationFrame+=1/60;
_painter.setAnimationFrame(animationFrame);
_paint.METHOD_I_DONT_KNOW_TO_FORCE_REDRAW();
// I want to avoid setState({animationFrame+=1/60;}); which works actually, but that doesn't sound very efficient to redraw the widget every 1/60 seconds, unless it's the right way to do it ?
}
#override
Widget build(BuildContext context) {
//developer.log('axis='+axis.toString(), name: 'DEBUG');
_painter=MyCustomPainter();
_painter.setAnimationFrame(animationFrame);
_paint=CustomPaint(
painter: _painter,
child: Container(),
);
return _paint;
}
}

Thx to #pskink for the hint in the comments, here is the working solution, using a ChangeNotifier when calling the constructor of the MyCustomPainter class.
class CustomAnimatedWidgetState extends State<CustomAnimatedWidget> {
CustomPaint _paint=null;
MyCustomPainter _painter=null;
ChangeNotifier _repaint=ChangeNotifier();
double animationFrame=0;
void tick() {
//called eery 1/60 seconds
animationFrame+=1/60;
_painter.setAnimationFrame(animationFrame);
_repaint.notifyListeners();
}
#override
Widget build(BuildContext context) {
_painter=MyCustomPainter(repaint:_repaint);
_painter.setAnimationFrame(animationFrame);
_paint=CustomPaint(
painter: _painter,
child: Container(),
);
return _paint;
}
}

Related

Differences between AnimatedBuilder and StatefulWidget in Flutter?

From my point of view, all animations continuously render the widget with some often-changed value. For example, a spinning hand on a clock has a value called 'angle' to indicate its position.
In Flutter, it seems that StatefulWidget is enough for it. My question is:
What functions do AnimatedBuilder/AnimatedWidget have?
What are the differences between AnimatedBuilder/AnimatedWidget and StatefulWidget?
I'll assume that AnimationBuilder is AnimatedBuilder because there is no such class as AnimationBuilder in the Flutter SDK.
Short answer
There are no differences besides the class names and the parameters.
Long answer
In Flutter, it seems that StatefulWidget is enough for it.
You are right.
What functions do AnimatedBuilder/AnimatedWidget have?
Nothing special, they are classes that exists only to wrap common/boilerplate code, see:
AnimatedWidget: flutter/lib/src/widgets/transitions.dart is simply a StatefulWidget that takes a listenable and triggers the setState whenever the listanable notifies a change.
The AnimatedBuilder: flutter/lib/src/widgets/transitions.dart is a subclass of ListenableBuilder which is a subclass of AnimatedWidget (!), the only difference is that AnimatedBuilder uses your callback as the build method of AnimatedWidget.
That being said, lets go to the code:
AnimatedBuilder is simply a StatefulWidget that uses your callback function (builder: (...) { }) as build method. It also triggers setState everytime the Listenable (animation) notifies a change.
Widget build(BuildContext context) {
return Center( // The [Center] widget instance will not rebuild.
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return /* Widge tree that will rebuild when [animation] changes. */;
},
),
);
}
The equivalent code using AnimatedWidget is:
Widget build(BuildContext context) {
return Center( // The [Center] widget instance will not rebuild.
child: MyAnimatedWidget(animation: animation),
);
}
// ...
class MyAnimatedWidget extends AnimatedWidget {
const MyAnimatedWidget({required Listenable animation}) : super(listenable: animation);
Widget build(BuildContext context) {
return /* Widge tree that will rebuild when [animation] changes. */;
}
}
What are the differences between AnimatedBuilder/AnimatedWidget and StatefulWidget?
As I said, there is no semantic or real difference. AnimatedWidget and AnimatedBuilder are only abstractions of StatefulWidget.

flutter slider not updating widget variables

am playing around with the slider widget on flutter, and I can't figure out why it does not update certain values in a different widget, example code is shown below;
When i move the slider, it has no issues moving, but the value i'm trying to update on the other widget does not update even though the onchanged is updating the variable passed through in a set state accordingly.
any help would be greatly appreciated!
Scaffold Code
class TestPage extends StatelessWidget {
static const id = "test_page";
#override
Widget build(BuildContext context) {
double testValue = 0;
return Scaffold(
body: Column(
children: [
Text("Hello World"),
TestBoxNumber(
numberDisplay: testValue,
),
TestSlider(testValue: testValue),
],
),
);
}
}
Slider Code
class TestSlider extends StatefulWidget {
double testValue;
TestSlider({required this.testValue});
#override
_TestSliderState createState() => _TestSliderState();
}
class _TestSliderState extends State<TestSlider> {
#override
Widget build(BuildContext context) {
return Slider(
activeColor: themeData.primaryColorLight,
value: widget.testValue,
min: 0,
max: 100,
divisions: 100,
label: widget.testValue.round().toString(),
onChanged: (double value) {
setState(() {
widget.testValue = value;
});
},
);
}
}
Different Widget Code
class TestBoxNumber extends StatefulWidget {
final double numberDisplay;
const TestBoxNumber({required this.numberDisplay});
#override
_TestBoxNumberState createState() => _TestBoxNumberState();
}
class _TestBoxNumberState extends State<TestBoxNumber> {
#override
Widget build(BuildContext context) {
return Container(
child: Text(widget.numberDisplay.toString()),
);
}
}
The problem is that you are constructing TestBoxNumber widget in such a way that value (testValue) will always be the same (testValue is never returned out of the TestSlider widget).
How to overcome this issue?
You can make your TestPage a StatefullWidget. Then create callback from TestSlider, so when you change value in TestSlider you will call some function in TestPage (with setState in it, causing re-rendering your page).
Or if you don't want your whole TestPage widget to be Statefull (if, let's say, you predict a lot of other static widgets in it and you don't want them to be re-rendered because you just moved a slider), you can create wrapper Statefull widget and put both TestSlider and TestBoxNumber widgets in it. This is more flexible approach, imho.
Here is small scheme of what I mean by wrapping two widgets in another one:
UPD: btw, there is no point in making TestBoxText a statefull widget if it's only purpose is to display a text and you pass it's value through the constructor.

How setState and shouldRepaint are coupled in CustomPainter?

Minimal reproducible code:
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<Offset> _points = [];
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() {}), // This setState works
child: Icon(Icons.refresh),
),
body: GestureDetector(
onPanUpdate: (details) => setState(() => _points.add(details.localPosition)), // but this doesn't...
child: CustomPaint(
painter: MyCustomPainter(_points),
size: Size.infinite,
),
),
);
}
}
class MyCustomPainter extends CustomPainter {
final List<Offset> points;
MyCustomPainter(this.points);
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.red;
for (var i = 0; i < points.length; i++) {
if (i + 1 < points.length) {
final p1 = points[i];
final p2 = points[i + 1];
canvas.drawLine(p1, p2, paint);
}
}
}
#override
bool shouldRepaint(MyCustomPainter oldDelegate) => false;
}
Try to draw something by long dragging on the screen, you won't see anything drawn. Now, press the FAB which will reveal the drawn painting maybe because FAB calls setState but onPanUpdate also calls setState and that call doesn't paint anything on the screen. Why?
Note: I'm not looking for a solution on how to enable the paint, a simple return true does the job. What I need to know is why one setState works (paints on the screen) but the other fails.
To understand why setState() in onPanUpdate is not working you might want to look into the widget paint Renderer i.e., CustomPaint.
The CustomPaint (As stated by docs as well) access the painter object (in your case MyCustomPainter) after finishing up the rendering of that frame. To confirm we can check the source of CustomPainter. we can see markNeedsPaint() is called only while we are accessing painter object through setter. For more clarity you might want to look into source of RenderCustomPaint , you will definitely understand it :
void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
// Check if we need to repaint.
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) { //THIS
markNeedsPaint();
}
.
.
.
}
While on every setState call your points are updating but every time creating new instances of 'MyCustomPainter` is created and the widget tree is already rendered but painter have not yet painted due to reason mentioned above.
That is why the only way to call markNeedPaint()(i.e., to paint your object), is by returning true to shouldRepaint or Either oldDeleagate is null which only happens and Fist UI build of the CustomPainter, you can verify this providing some default points in the list.
It is also stated that
It's possible that the [paint] method will get called even if
[shouldRepaint] returns false (e.g. if an ancestor or descendant
needed to be repainted). It's also possible that the [paint] method
will get called without [shouldRepaint] being called at all (e.g. if
the box changes size).
So the only reason of setState of Fab to be working here (which seams valid) is that Fab is somehow rebuilding the any parent of the custom painter. You can also try to resize the UI in 'web build' or using dartpad you will find that as parent rebuilds itself the points will become visible So setState directly have nothing to do with shouldRepaint. Even hovering on the fab (in dartpad) button will cause the ui to rebuild and hence points will be visible.

Will my whole widget tree rebuild when a keyboard appears?

I am trying to build a responsive mobile app so I found an approach were i would divide the sreen into definite number of grids and get the grid width and height and then use this width and height to size my widgets
Question:
I would definitly get my screen's size from MediaQuery.of(context) but since i will only use it once to do my calculations will my widget tree rebuild (assuming i did this calculation in my root widget) whenever a keyboard appears or not? And if it will rebuild should i do the calculations in a different place?
No, if you didn't place any set state or callback during that rebuild the widget when you open the keyboard. However, the issue can be easily resolved by putting your main widget "below" the Scaffold in a SingleChildScrollView to avoid rendering issues.
If you absolutely need to perform actions when the keyboard appears you can use a FocusNode in the textField and add a listener to it with the addListener method. By passing a function to add Listener, you can trigger a setState every time you need, causing the widget to rebuild with the new parameters.
This is a very simplified version of what I mean:
class MyWidget extends StatefulWidget{
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
FocusNode _focusNode;
int state=0;
#override
Widget build(BuildContext context) {
return Container(
height: state==0?100:200, //change the height depending on "state"
child: TextField(
focusNode: _focusNode,
),
);
}
void onFocus(){
setState(() {
//Check if the focus node is focused
if(_focusNode.hasFocus) state=1; //Change the value of the state
});
}
#override
void initState() {
super.initState();
_focusNode=FocusNode();
_focusNode.addListener(onFocus); //Here on focus will be called
}
#override
void dispose() {
super.dispose();
_focusNode.dispose();
}
}

Persistent Ticker in Flutter

How to get a persistent tick at every frame refresh time. For example in Flame game engine update method gets called at around every 1/60 seconds and a value dt with elapsed time is passed.
I want to implement one simple animation where a fan will rotate. I want to change its rotation speed depending on user input. My idea is that at every tick I will rotate the fan image/ container at a fixed value. As the user increases the speed I will increase the multiplier. There are few options like using the Flame engine or Flare, but they seem overkill. Also, I can use SingleTickerProviderMixin but there are few overheads like reverse the animation when finished and forwarded it and so...
I think there will be a simple solution, which will notify me at each frame refresh time that occurs at around every 1/60 seconds, and pass me the elapsed time dt (around 167 mS or so).
A nice way to do it (without Animation widgets), is to implement a Timer with a Stream; see the example below:
import 'package:flutter/material.dart';
import "dart:async";
const frequency = Duration(milliseconds: 50);
void main() => runApp(
MaterialApp(
home: Material(
child: Center(
child: Container(
color: Colors.white,
child: MyWidget(),
),
),
),
),
);
class MyWidget extends StatefulWidget {
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
final StreamController<double> _streamer =
StreamController<double>.broadcast();
Timer timer;
double _rotation = 0.0;
#override
void initState() {
super.initState();
timer = Timer.periodic(frequency, (t) {
_rotation++;
_streamer.add(1);
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder<double>(
initialData: 0,
stream: _streamer.stream,
builder: (context, snapshot) {
return Transform(
transform: Matrix4.rotationZ(_rotation),
child: Text('Hello, World!'),
);
});
}
}
I would also make sure to implement the dispose() callback if you copy this code. You need to make sure to cancel() any running timers to prevent odd behaviors or they will become a source of memory leaks.
The timer = null; is not always needed, but there are situations where the state object will hold a reference to the timer var itself and also cause a memory leak. For example, if you capture the timer var inside the timer callback body.
Example:
#override
void dispose() {
timer?.cancel();
timer = null;
super.dispose();
}