Flutter: CustomPainter paint method gets called several times instead of only once

1,110

Solution 1

A poor solution might be to add a RepaintBoundary around the hover Widgets:

class _MyHomePageState extends State<MyHomePage> {
  MaterialColor actualColor = Colors.red;

  @override
  Widget build(BuildContext context) {
    print('Rebuilding with $actualColor');
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPainter Demo'),
        actions: <Widget>[
          RepaintBoundary(
            child: OutlinedButton(
                style: ButtonStyle(
                    foregroundColor: MaterialStateProperty.all(Colors.black)),
                onPressed: () {
                  setState(() => actualColor = Colors.red);
                },
                child: Text('RedCircle')),
          ),
          RepaintBoundary(
            child: OutlinedButton(
                style: ButtonStyle(
                    foregroundColor: MaterialStateProperty.all(Colors.black)),
                onPressed: () {
                  setState(() => actualColor = Colors.green);
                },
                child: Text('GreenCircle')),
          ),
        ],
      ),
      body: Center(
        child: CustomPaint(
          size: Size(300, 300),
          painter: ColorCircle(myColor: actualColor),
        ),
      ),
    );
  }
}

And then, to properly define the shouldRepaint method of the ColorCircle (currently returning false):

@override
bool shouldRepaint(CustomPainter oldDelegate) {
  return (oldDelegate as ColorCircle).myColor != myColor;
}

This seems to be a really poor solution. I would be interested to know of a better, more sustainable answer.

Full source code with RepaintBoundary workaround

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'CustomPainter Demo',
      home: MyHomePage(),
    );
  }
}

class ColorCirle extends CustomPainter {
  MaterialColor myColor;

  ColorCirle({@required this.myColor});
  @override
  void paint(Canvas canvas, Size size) {
    debugPrint('ColorCircle.paint, ${DateTime.now()}');
    final paint = Paint()..color = myColor;
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), 100, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return (oldDelegate as ColorCirle).myColor != myColor;
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  MaterialColor actualColor = Colors.red;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPainter Demo'),
        actions: <Widget>[
          RepaintBoundary(
            child: OutlinedButton(
                style: ButtonStyle(
                    foregroundColor: MaterialStateProperty.all(Colors.black)),
                onPressed: () {
                  setState(() => actualColor = Colors.red);
                },
                child: Text('RedCircle')),
          ),
          RepaintBoundary(
            child: OutlinedButton(
                style: ButtonStyle(
                    foregroundColor: MaterialStateProperty.all(Colors.black)),
                onPressed: () {
                  setState(() => actualColor = Colors.green);
                },
                child: Text('GreenCircle')),
          ),
        ],
      ),
      body: Center(
        child: CustomPaint(
          size: Size(300, 300),
          painter: ColorCirle(myColor: actualColor),
        ),
      ),
    );
  }
}

Solution 2

There 2 things you need to do:

  1. warp the CustomPaint with RepaintBoundary

    Center(
     child: RepaintBoundary(
       child: CustomPaint(
         size: Size(300, 300),
         painter: ColorCircle(myColor: actualColor),
       ),
     ),
    
  2. return true for shouldRepaint method

    bool shouldRepaint(CustomPainter oldDelegate) {
      return true;
    }
    
Share:
1,110
Norman
Author by

Norman

Exploring App Development (Flutter, Android)

Updated on December 28, 2022

Comments

  • Norman
    Norman over 1 year

    I have a simple app that draws via a CustomPainter a red or green circle on a canvas, depending on which button is pressed in the AppBar:

    Red Circle
    Green Circle

    The class ColorCircle extends CustomPainter and is responsible for drawing the colored circle:

    class ColorCircle extends CustomPainter {
      MaterialColor myColor;
    
      ColorCircle({@required this.myColor});
      
      @override
      void paint(Canvas canvas, Size size) {
        debugPrint('ColorCircle.paint, ${DateTime.now()}');
        final paint = Paint()..color = myColor;
        canvas.drawCircle(Offset(size.width / 2, size.height / 2), 100, paint);
      }
    
      @override
      bool shouldRepaint(CustomPainter oldDelegate) => false;
    }
    

    The drawing of the different colors works fine, but when I click (only once!) or hover over one of the buttons, the paint method gets called several times:

    Debugmessage


    Further implementation details: I use a StatefulWidget for storing the actualColor. In the build method actualColor is passed to the ColorCircle constructor:

    class _MyHomePageState extends State<MyHomePage> {
      MaterialColor actualColor = Colors.red;
        
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            actions: <Widget>[
              OutlinedButton(
                onPressed: () => setState(() => actualColor = Colors.red),
                child: Text('RedCircle'),
              ),
              OutlinedButton(
                onPressed: () => setState(() => actualColor = Colors.green),
                child: Text('GreenCircle'),
              ),
            ],
          ),
          body: Center(
            child: CustomPaint(
              size: Size(300, 300),
              painter: ColorCircle(myColor: actualColor),
            ),
          ),
        );
      }
    }  
    

    The complete source code with a running example can be found here: CustonPainter Demo


    So why is paint called several times instead of only once? (And how could you implement it so that paint is called only once?).

    • frankenapps
      frankenapps about 3 years
      Actually the repaint is happening whenever you hover the mouse above any of the buttons, or at least when you hover from one button to another.
  • Norman
    Norman about 3 years
    RepaintBoundary does not seem to work. The color is not changing when the buttons are clicked.
  • Thierry
    Thierry about 3 years
    Correct. Sorry. I will check that.
  • Thierry
    Thierry about 3 years
    Interestingly, RepaintBoundary works but the other way around. If you add a RepaintBoundary around the action buttons as in my updated answer. But this is really not practical. I will add a bounty to your question as I am interested in the answer too.
  • Parth Dave
    Parth Dave about 3 years
    Sorry no solution for now other then this as issue is still pending at framework level: github.com/flutter/flutter/issues/49298