Flutter中如何优化基于CustomPainter的手绘应用性能?
手绘Canvas性能优化问题
我正在实现一个让用户可在屏幕画布上自由手绘的功能,目前基础功能运行正常——通过canvas.saveLayer和canvas.restore可以切换笔刷大小、颜色,也能实现擦除。但核心问题是:当绘图积累到一定量后,paint方法的开销会急剧上升,导致应用卡顿,极端情况下帧率只剩1fps。我试过用RepaintBoundary,但因为每次绘图都会重绘整个画布,所以没什么效果;也尝试过避免向列表添加重复Offset,但只有微小改善,无法解决根本问题。
相关代码如下:
class DrawingCanvas extends StatefulWidget { const DrawingCanvas({ super.key, required this.onPanUpdate, required this.onPanEnd, required this.initialDrawings, required this.selectedPainter, this.boundarySize = Size.infinite, }); final List<DrawingDetails> initialDrawings; final void Function(Offset?) onPanUpdate; final void Function(Offset?) onPanEnd; final Paint selectedPainter; final Size boundarySize; @override State<DrawingCanvas> createState() => _DrawingCanvasState(); } class _DrawingCanvasState extends State<DrawingCanvas> { @override Widget build(BuildContext context) { return GestureDetector( onPanStart: (details) { setState(() { widget.onPanUpdate(details.localPosition); }); }, onPanUpdate: (details) { setState(() { widget.onPanUpdate(details.localPosition); }); }, onPanEnd: (details) { setState(() { widget.onPanEnd(null); }); }, child: CustomPaint( isComplex: true, painter: _DrawingPainter( initialDrawings: widget.initialDrawings, selectedPainter: widget.selectedPainter, boundarySize: widget.boundarySize, ), size: Size.infinite, ), ); } } class _DrawingPainter extends CustomPainter { final List<DrawingDetails> initialDrawings; final Paint selectedPainter; final Logger logger = Logger('_DrawingPainter'); final Size boundarySize; _DrawingPainter({ required this.initialDrawings, required this.selectedPainter, required this.boundarySize, }); @override bool shouldRepaint(_DrawingPainter oldDelegate) { return true; } void paint(Canvas canvas, Size size) { canvas.saveLayer(Rect.largest, Paint()); for (DrawingDetails drawing in initialDrawings) { for (int i = 0; i < drawing.points.length - 1; i++) { if (drawing.points[i] != null && drawing.points[i + 1] != null) { canvas.drawLine( drawing.points[i]!, drawing.points[i + 1]!, drawing.painterOptions.painter, ); } } } canvas.restore(); } }
优化方案
1. 缓存历史绘制内容,避免重复重绘
每次用户完成一笔绘制(触发onPanEnd)时,将已完成的所有历史内容渲染到Picture对象中缓存起来。后续重绘时,只需先绘制缓存的Picture,再绘制当前正在进行的那一笔即可,无需重复绘制所有历史线条。
示例修改:
// 在_DrawingCanvasState中添加缓存变量 Picture? _cachedHistory; // 当完成一笔时生成缓存 void _updateCache() { final recorder = PictureRecorder(); final canvas = Canvas(recorder); // 只绘制已完成的绘图(假设DrawingDetails有isCompleted标记) for (final drawing in widget.initialDrawings.where((d) => d.isCompleted)) { final path = Path(); Offset? lastPoint; for (final point in drawing.points) { if (point == null) continue; if (lastPoint == null) { path.moveTo(point.dx, point.dy); } else { path.lineTo(point.dx, point.dy); } lastPoint = point; } canvas.drawPath(path, drawing.painterOptions.painter); } setState(() { _cachedHistory = recorder.endRecording(); }); } // 在onPanEnd时调用缓存更新 @override Widget build(BuildContext context) { return GestureDetector( onPanEnd: (details) { setState(() { widget.onPanEnd(null); _updateCache(); }); }, // 其他手势逻辑不变 child: CustomPaint( isComplex: true, painter: _DrawingPainter( initialDrawings: widget.initialDrawings, selectedPainter: widget.selectedPainter, boundarySize: widget.boundarySize, cachedHistory: _cachedHistory, ), size: Size.infinite, ), ); } // 修改_DrawingPainter接收缓存 class _DrawingPainter extends CustomPainter { final Picture? cachedHistory; // 其他参数不变 _DrawingPainter({ required this.initialDrawings, required this.selectedPainter, required this.boundarySize, this.cachedHistory, }); @override void paint(Canvas canvas, Size size) { // 先绘制缓存的历史内容 if (cachedHistory != null) { canvas.drawPicture(cachedHistory!); } // 再绘制当前未完成的那一笔 final currentDrawing = widget.initialDrawings.lastWhereOrNull((d) => !d.isCompleted); if (currentDrawing != null) { final path = Path(); Offset? lastPoint; for (final point in currentDrawing.points) { if (point == null) continue; if (lastPoint == null) { path.moveTo(point.dx, point.dy); } else { path.lineTo(point.dx, point.dy); } lastPoint = point; } canvas.drawPath(path, currentDrawing.painterOptions.painter); } } }
2. 优化shouldRepaint逻辑
当前shouldRepaint无条件返回true,导致任何状态变化都会触发全量重绘。改为仅当绘图内容真正变化时才返回true:
@override bool shouldRepaint(_DrawingPainter oldDelegate) { if (initialDrawings.length != oldDelegate.initialDrawings.length) return true; // 对比每一项绘图的内容和画笔是否变化 for (int i = 0; i < initialDrawings.length; i++) { final current = initialDrawings[i]; final old = oldDelegate.initialDrawings[i]; if (current.points.length != old.points.length || current.painterOptions != old.painterOptions || current.isCompleted != old.isCompleted) { return true; } } return cachedHistory != oldDelegate.cachedHistory; }
3. 移除不必要的saveLayer
如果没有全局混合、滤镜等需求,canvas.saveLayer(Rect.largest, Paint())会额外创建图层,增加性能开销,直接移除即可。
4. 合并线条为Path绘制
多次调用drawLine的效率远低于绘制一个完整的Path。将每一笔的所有点合并成一个Path,再用drawPath一次性绘制,能显著减少绘制调用次数。
示例:
// 为DrawingDetails添加方法生成Path Path get path { final path = Path(); Offset? lastPoint; for (final point in points) { if (point == null) continue; if (lastPoint == null) { path.moveTo(point.dx, point.dy); } else { path.lineTo(point.dx, point.dy); } lastPoint = point; } return path; } // 在paint中使用Path绘制 canvas.drawPath(drawing.path, drawing.painterOptions.painter);
内容的提问来源于stack exchange,提问作者Breno Veríssimo
相关产品推荐
相关产品推荐

