iDecode
iDecode

Reputation: 28906

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.

Upvotes: 2

Views: 1354

Answers (1)

Sahdeep Singh
Sahdeep Singh

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

Related Questions