Reputation: 28906
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.
Upvotes: 2
Views: 1354
Reputation: 1442
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.
Upvotes: 1