I am working on a project that has a CustomPaint
that draws shapes as a background, and when you tap the screen, it draws a circle on that specific position. I am using GestureDetector
to get the tap data and send it as an argument to _MyCustomPainter
Something similar to the code below:
class MyApp extends StatefulWidget{
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Offset _circlePosition = Offset(-1, -1);
void setCirclePosition(Offset newPosition) {
setState(() {
this._circlePosition = newPosition;
});
}
void clearCircle() {
setState(() {
this._circlePosition = Offset(-1, -1);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.green,
title: Text('My app'),
),
body: Container(
child: GestureDetector(
onTapDown: (detail) {
setCirclePosition(detail.localPosition);
},
onHorizontalDragStart: (detail) {
setCirclePosition(detail.localPosition);
},
onHorizontalDragUpdate: (detail) {
setCirclePosition(detail.localPosition);
},
onVerticalDragStart: (detail) {
setCirclePosition(detail.localPosition);
},
onVerticalDragUpdate: (detail) {
setCirclePosition(detail.localPosition);
},
onTapUp: (detail) {
clearCircle();
},
onVerticalDragEnd: (detail) {
clearCircle();
},
onHorizontalDragEnd: (detail) {
clearCircle();
},
child: LimitedBox(
maxHeight: 400,
maxWidth: 300,
child: CustomPaint(
size: Size.infinite,
painter: new _MyCustomPainter(
circlePosition: circlePosition,
),
),
),
),
),
),
);
}
}
class _MyCustomPainter extends CustomPainter {
_MyCustomPainter({
this.circlePosition,
});
final Offset circlePosition;
void _drawBackground(Canvas canvas) {
// draw the background
}
@override
void paint(Canvas canvas, Size size) {
_drawBackground(canvas);
if (circlePosition.dx != -1 && circlePosition.dy != -1) {
// draws a circle on the position
var circlePaint = Paint()..color = Colors.green;
canvas.drawCircle(circlePosition, 5, circlePaint);
}
}
@override
bool shouldRepaint(_MyCustomPainter old) {
return circlePosition.dx != old.circlePosition.dx ||
circlePosition.dy != old.circlePosition.dy;
}
}
So the thing here is, every time the user moves the finger on the screen with a long tap the background that doesn't change is being repainted over and over, and there is a lot of code involved in painting this specific background. There is the shouldRepaint
method but it signals to repaint the whole widget. How could I make so that the background is only drawn once, and than on repaint I just use the background I created previously? Is using a PictureRecorder
to generate an image and than using that image on future repaints the best way to do this? and also should my CustomPainter
extend ChangeNotifier
for better performance? The current approach works, but I am wondering how could I improve it.
So the approach I used to achieve this is having two CustomPainters, one for the background (let's call it BackgroundPainter) and one for the foregroud (like ForegroundPainter, and when the BackgroundPaiter.shouldRepaint method returns true, you draw it in a canvas and save it to an image, and use the image on the ForegroundPainter. I had to do this for this candlesticks chart widget here. A short version of the code would be something like the example bellow:
class MyWidget extends StatefulWidget {
MyWidget({
this.foregroundParam,
this.backgroundParam,
});
final String foregroundParam;
final String backgroundParam;
@override
MyWidget createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Picture backgroundPicture;
_BackgroundPainter backgroundPainter;
Size parentSize;
Size oldParentSize;
@override
Widget Build(BuildContext context) {
// We need the context size, which is only available after
// the build method finishes.
WidgetsBinding.instance.addPostFrameCallback((_) {
// this code only runs after build method is run
if (parentSize != context.size) {
setState(() {
parentSize = context.size;
});
}
});
var newBackgroundPainter = _BackgroundPainter(
backgroundParam: widget.backgroundParam,
);
// update backgroundPicture and backgroundPainter if parantSize was updated
// (like after a screen rotation) or if backgroundPainter was updated
// based on the backgroundParam
if (parentSize != null &&
(oldParentSize != null && oldParentSize != parentSize ||
backgroundPainter == null ||
newBackgroundPainter.shouldRepaint(backgroundPainter) ||
backgroundPicture == null)
) {
oldParentSize = parentSize;
backgroundPainter = newBackgroundPainter;
var recorder = PictureRecorder();
var canvas = Canvas(recorder, Rect.fromLTWH(0, 0, parentSize.width, parentSize.height));
backgroundPainter.paint(canvas, parentSize);
setState(() {
backgroundPicture = recorder.endRecording();
});
}
// if there is no backgroundPicture, this must be the first run where
// parentSize was not set yet, so return a loading screen instead, but
// this is very quick and most likely will not even be noticed. Could return
// an empty Container as well
if (backgroundPicture == null) {
return Container(
width: double.infinity,
height: double.infinity,
child: Center(
child: CircularProgressIndicator(),
),
);
}
// if everything is set, return an instance of _ForegroundPainter passing
// the backgroundPicture as parameter
return CustomPaint(
size: Size.infinite,
painter: _ForegroundPainter(
backgroundPicture: backgroundPicture,
foregroundParam: widget.foregroundParam,
),
);
}
}
class _BackgroundPainter extends CustomPainter {
_BackgroundPainter({
this.backgroundParam,
});
final String backgroundParam;
@override
void paint(Canvas canvas, Size size) {
// paint background here
}
@override
bool shouldRepaint(_BackgroundPainter oldPainter) {
return backgroundParam != oldPainter.backgroundParam;
}
}
class _ForegroundPainter extends CustomPainter {
_ForegroundPainter({
this.backgroundPicture,
this.foregroundParam,
});
final Picture backgroundPicture;
final String foregroundParam;
@override
void paint(Canvas canvas, Size size) {
canvas.drawPicture(backgroundPicture);
// paint foreground here
}
@override
bool shouldRepaint(_ForegroundPainter oldPainter) {
return foregroundParam != oldPainter.foregroundParam ||
backgroundPicture != oldPainter.backgroundPicture;
}
}