Reputation: 3157
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
:
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:
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?).
Upvotes: 11
Views: 3092
Reputation: 3524
All you need to do is to warp the CustomPaint
with RepaintBoundary
Center(
child: RepaintBoundary(
child: CustomPaint(
size: Size(300, 300),
painter: ColorCircle(myColor: actualColor),
),
),
By default CustomPainter
is in the same layer as every other widget on the same screen so it's paint method will get called if any other widget on the same screen repaint.
To fix this we can isolate the CustomPainter
with RepaintBoundary
so any repainting outside this RepaintBoundary
wont effect it, or we can fix it by warping other widgets that would repaint with RepaintBoundary
so they won't effect any other widgets (including the CustomPainter
widget) when they get repaint, however it's better to just warp the CustomPainter
with the RepaintBoundary
instead of warping multiple widgets with RepaintBoundary
since it's costly and sometimes have no effect.
You can get a better view and understanding of this by enabling Highlight repaints in the DevTools
.
Upvotes: 22
Reputation: 8383
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.
RepaintBoundary
workaroundimport '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),
),
),
);
}
}
Upvotes: 2