Flutter: CustomPainter paint method gets called several times instead of only once
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:
warp the
CustomPaint
withRepaintBoundary
Center( child: RepaintBoundary( child: CustomPaint( size: Size(300, 300), painter: ColorCircle(myColor: actualColor), ), ),
return true for
shouldRepaint
methodbool shouldRepaint(CustomPainter oldDelegate) { return true; }
Comments
-
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 theAppBar
:The class
ColorCircle
extendsCustomPainter
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:
Further implementation details: I use a
StatefulWidget
for storing theactualColor
. In the build methodactualColor
is passed to theColorCircle
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 thatpaint
is called only once?).-
frankenapps about 3 yearsActually 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 about 3 years
RepaintBoundary
does not seem to work. The color is not changing when the buttons are clicked. -
Thierry about 3 yearsCorrect. Sorry. I will check that.
-
Thierry about 3 yearsInterestingly, 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 about 3 yearsSorry no solution for now other then this as issue is still pending at framework level: github.com/flutter/flutter/issues/49298