Search code examples
flutterdartcanvasgraphicsflutter-layout

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


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?).


Solution

  • 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),
            ),
          ),
        );
      }
    }