I have an app that displays a black dot at the point where the user touches the screen like this:
The black dot can be moved by the user as he/she drags his finger on the screen.
The background is an expensive paint operation, so I have created two separate widgets in a stack, hoping that the background widget painting will be stored in the Flutter raster cache. But it's not stored - Flutter calls my expensive paint method every time the black dot moves.
What am I doing wrong?
Here's my code:
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
GlobalKey _paintKey = new GlobalKey();
Offset _offset;
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Stack(
fit: StackFit.expand,
children: <Widget>[
new CustomPaint(
painter: new ExpensivePainter(),
isComplex: true,
willChange: false,
),
new Listener(
onPointerDown: _updateOffset,
onPointerMove: _updateOffset,
child: new CustomPaint(
key: _paintKey,
painter: new MyCustomPainter(_offset),
child: new ConstrainedBox(
constraints: new BoxConstraints.expand(),
),
),
)
],
),
);
}
_updateOffset(PointerEvent event) {
RenderBox referenceBox = _paintKey.currentContext.findRenderObject();
Offset offset = referenceBox.globalToLocal(event.position);
setState(() {
_offset = offset;
});
}
}
class ExpensivePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print("Doing expensive paint job");
Random rand = new Random(12345);
List<Color> colors = [
Colors.red,
Colors.blue,
Colors.yellow,
Colors.green,
Colors.white,
];
for (int i = 0; i < 5000; i++) {
canvas.drawCircle(
new Offset(
rand.nextDouble() * size.width, rand.nextDouble() * size.height),
10 + rand.nextDouble() * 20,
new Paint()
..color = colors[rand.nextInt(colors.length)].withOpacity(0.2));
}
}
@override
bool shouldRepaint(ExpensivePainter other) => false;
}
class MyCustomPainter extends CustomPainter {
final Offset _offset;
MyCustomPainter(this._offset);
@override
void paint(Canvas canvas, Size size) {
if (_offset == null) return;
canvas.drawCircle(_offset, 10.0, new Paint()..color = Colors.black);
}
@override
bool shouldRepaint(MyCustomPainter other) => other._offset != _offset;
}
To be able to use the CustomPaint widget, you need to create a class that extends the CustomPainter. The class would have to implement two methods paint () and shouldRepaint (), example: As you can see above, the paint () method will get called whenever the objects needs to repaint and it will have two parameters Canvas and Size.
When using CustomPaint, you have two choices either specify a size property without a child or use the child property giving it a widget. To be able to use the CustomPaint widget, you need to create a class that extends the CustomPainter. The class would have to implement two methods paint () and shouldRepaint (), example:
When asked to paint, CustomPaint first asks its painter to paint on the current canvas, then it paints its child, and then, after painting its child, it asks its foregroundPainter to paint. The coordinate system of the canvas matches the coordinate system of the CustomPaint object.
What is CustomPaint? CustomPaint is a widget from the Flutter SDK, which enables you to use a canvas to draw different shapes. It contains the following properties: painter: The painter that paints before the child. Here you would need to create a class that extends the class CustomPainter
It's a specificity of Flutter. We are not in React, where "Components" are repainted only when their state/props change.
In Flutter, every time a widget has to repaint the whole tree will too.
Usually, this is not a problem and fairly fast. But in some cases (such as yours), you don't want that. And this is where a fairly undocumented but important widget appears! RepaintBoundary
There's an excellent talk about how Flutter's rendering pipeline works, here: https://www.youtube.com/watch?v=UUfXWzp0-DU
But in short, consider RepaintBoundary
as what tells Flutter to split the painting operation into different parts.
Anyway, the solution ?
Wrap your Expensive
widget in a RepaintBoundary
. And suddenly you get 60 FPS.
new RepaintBoundary(
child: new CustomPaint(
painter: new ExpensivePainter(),
isComplex: true,
willChange: false,
),
),
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With