Flutter绘图性能优化求助:GestureDetector+CustomPaint绘图卡顿问题
Hey there! Let's tackle this performance issue you're seeing with your gesture-driven drawing in Flutter. The root cause here is almost always related to unnecessary repaints or an ever-growing list of points that your CustomPainter has to process every frame. Here are the key optimizations you can implement to fix the stuttering:
1. Fix shouldRepaint to avoid unnecessary repaints
First, make sure your DrawPainting painter correctly implements shouldRepaint. If you're returning true unconditionally right now, it'll repaint every single frame even if the points haven't changed. Update it to only repaint when the point list actually differs:
class DrawPainting extends CustomPainter { final List<Offset> points; final Paint _paint; // Reuse this paint object instead of creating it every time DrawPainting(this.points) : _paint = Paint() ..color = Colors.blue ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round; @override void paint(Canvas canvas, Size size) { for (int i = 0; i < points.length - 1; i++) { if (points[i] != null && points[i+1] != null) { canvas.drawLine(points[i], points[i+1], _paint); } } } @override bool shouldRepaint(DrawPainting oldDelegate) { // Only repaint if the point lists are different if (oldDelegate.points.length != points.length) return true; for (int i = 0; i < points.length; i++) { if (oldDelegate.points[i] != points[i]) return true; } return false; } }
Notice we also moved the Paint initialization to the constructor—this avoids creating a new Paint object on every paint call, a small change that adds up over time.
2. Reduce the number of points you're storing
onPanUpdate fires very frequently (multiple times per millisecond), so your _points list can grow to thousands of entries quickly. Most of these points are redundant because they're extremely close together. Add a distance threshold to only add a new point if it's far enough from the last one:
// In your State class List<Offset> _points = []; void _onPanUpdate(DragUpdateDetails details) { RenderBox renderBox = context.findRenderObject() as RenderBox; Offset localPosition = renderBox.globalToLocal(details.globalPosition); // Only add the point if it's at least 1.5px away from the last entry if (_points.isEmpty || (_points.last - localPosition).distance > 1.5) { setState(() { _points.add(localPosition); }); } }
You can adjust the distance threshold based on your needs—higher values mean fewer points (better performance) with slightly less line smoothness, lower values keep smoothness but add more points.
3. Wrap your CustomPaint in a RepaintBoundary
By default, if the parent widget repaints, your CustomPaint will too. Wrapping it in a RepaintBoundary isolates the drawing area so it only repaints when its own content changes:
@override Widget build(BuildContext context) { return Container( constraints: const BoxConstraints.expand(), child: GestureDetector( onPanUpdate: _onPanUpdate, child: RepaintBoundary( // Add this to isolate repaints child: CustomPaint( painter: DrawPainting(_points), ), ), ), ); }
4. Avoid recreating the DrawPainting instance unnecessarily
In your original code, you're creating a new DrawPainting every time build runs. Instead, use a ValueListenableBuilder to only update the painter when _points changes, which avoids unnecessary object instantiations:
// In your State class, switch to a ValueNotifier for points final ValueNotifier<List<Offset>> _pointsNotifier = ValueNotifier([]); void _onPanUpdate(DragUpdateDetails details) { RenderBox renderBox = context.findRenderObject() as RenderBox; Offset localPosition = renderBox.globalToLocal(details.globalPosition); if (_pointsNotifier.value.isEmpty || (_pointsNotifier.value.last - localPosition).distance > 1.5) { _pointsNotifier.value = List.from(_pointsNotifier.value)..add(localPosition); } } @override Widget build(BuildContext context) { return Container( constraints: const BoxConstraints.expand(), child: GestureDetector( onPanUpdate: _onPanUpdate, child: RepaintBoundary( child: ValueListenableBuilder<List<Offset>>( valueListenable: _pointsNotifier, builder: (context, points, child) { return CustomPaint(painter: DrawPainting(points)); }, ), ), ), ); }
This way, the CustomPaint only gets a new painter when the point list actually changes, not on every build of the parent widget.
5. Optional: Use a more efficient point storage
If you're still seeing issues with very long drawing sessions, consider using a Float32List instead of a List<Offset> to store points—this uses less memory and can be faster to iterate over:
// In your State class Float32List _pointsBuffer = Float32List(0); void _onPanUpdate(DragUpdateDetails details) { RenderBox renderBox = context.findRenderObject() as RenderBox; Offset localPosition = renderBox.globalToLocal(details.globalPosition); if (_pointsBuffer.isEmpty || (Offset(_pointsBuffer[_pointsBuffer.length-2], _pointsBuffer[_pointsBuffer.length-1]) - localPosition).distance > 1.5) { setState(() { _pointsBuffer = Float32List.fromList([ ..._pointsBuffer, localPosition.dx, localPosition.dy, ]); }); } } // Update the painter to use Float32List class DrawPainting extends CustomPainter { final Float32List pointsBuffer; final Paint _paint; DrawPainting(this.pointsBuffer) : _paint = Paint() ..color = Colors.blue ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round; @override void paint(Canvas canvas, Size size) { for (int i = 0; i < pointsBuffer.length - 2; i += 2) { Offset p1 = Offset(pointsBuffer[i], pointsBuffer[i+1]); Offset p2 = Offset(pointsBuffer[i+2], pointsBuffer[i+3]); canvas.drawLine(p1, p2, _paint); } } @override bool shouldRepaint(DrawPainting oldDelegate) { return !listEquals(oldDelegate.pointsBuffer, pointsBuffer); } }
This reduces the overhead of storing individual Offset objects, which helps with memory usage and iteration speed for very large datasets.
Give these changes a try—most people see a huge improvement in performance after implementing the first three optimizations alone!
内容的提问来源于stack exchange,提问作者Manukumar S B

