Flutter自定义Canvas绘制优化:少量点绘制后卡顿问题求助
解决Flutter自定义Canvas绘制大量点时的卡顿问题
1. 修复shouldRepaint的无差别重绘
当前shouldRepaint始终返回true,哪怕没有任何点的变化也会触发重绘,这是不必要的性能消耗。修改为仅当点列表真正变化时才重绘:
import 'package:flutter/foundation.dart'; class BrushPainter extends CustomPainter { final List<PainterPoint> points; BrushPainter({required this.points}); // ... 其他代码不变 ... @override bool shouldRepaint(covariant BrushPainter oldDelegate) { // 利用listEquals对比列表内容是否一致 return !listEquals(oldDelegate.points, points); } }
2. 预合并路径,减少实时计算量
每次paint时遍历所有点构建路径的开销极大,尤其是点数量多的时候。可以提前将连续的、同属性(颜色、粗细、混合模式)的点合并成完整路径并缓存,绘制时直接用缓存好的路径和Paint:
首先新增一个缓存类存储路径和对应的绘制属性:
class CachedPath { final Path path; final Paint paint; CachedPath(this.path, this.paint); }
在你的Widget中维护缓存列表,并在点列表更新时预合并路径:
List<CachedPath> _cachedPaths = []; void _updateCachedPaths() { _cachedPaths.clear(); if (points.isEmpty) return; Path currentPath = Path(); Paint currentPaint = Paint() ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; bool isNewLine = true; for (final point in points) { if (point.isEnd) { if (!isNewLine) { // 复制路径和Paint,避免后续修改影响缓存 _cachedPaths.add(CachedPath( Path.from(currentPath), Paint()..from(currentPaint), )); currentPath.reset(); isNewLine = true; } continue; } // 检查当前点的属性是否和缓存的Paint一致,不一致则先保存当前路径 bool paintMismatch = point.color != currentPaint.color || point.strokeSize != currentPaint.strokeWidth || (point.isEraser ? currentPaint.blendMode != BlendMode.clear : currentPaint.blendMode != BlendMode.color); if (paintMismatch && !isNewLine) { _cachedPaths.add(CachedPath( Path.from(currentPath), Paint()..from(currentPaint), )); currentPath.reset(); isNewLine = true; } if (isNewLine) { currentPath.moveTo(point.offset.dx, point.offset.dy); isNewLine = false; // 更新Paint属性 if (point.isEraser) { currentPaint.color = Colors.transparent; currentPaint.blendMode = BlendMode.clear; } else { currentPaint.color = point.color; currentPaint.blendMode = BlendMode.color; } currentPaint.strokeWidth = point.strokeSize; } else { currentPath.lineTo(point.offset.dx, point.offset.dy); } } // 保存最后一段未结束的路径 if (!isNewLine) { _cachedPaths.add(CachedPath( Path.from(currentPath), Paint()..from(currentPaint), )); } }
然后修改BrushPainter直接使用缓存的路径:
class BrushPainter extends CustomPainter { final List<CachedPath> cachedPaths; // 可选:维护当前正在绘制的临时路径,避免每次都重新合并所有缓存 final Path? tempPath; final Paint? tempPaint; BrushPainter({required this.cachedPaths, this.tempPath, this.tempPaint}); @override void paint(Canvas canvas, Size size) { // 绘制所有缓存好的路径 for (final cached in cachedPaths) { canvas.drawPath(cached.path, cached.paint); } // 绘制当前正在进行的临时路径 if (tempPath != null && tempPaint != null) { canvas.drawPath(tempPath!, tempPaint!); } } @override bool shouldRepaint(covariant BrushPainter oldDelegate) { // 简单对比缓存数量,或者更精细对比内容 if (cachedPaths.length != oldDelegate.cachedPaths.length) return true; // 检查最后一个缓存路径是否变化(针对实时绘制的场景) if (cachedPaths.isNotEmpty && oldDelegate.cachedPaths.isNotEmpty) { final last = cachedPaths.last; final oldLast = oldDelegate.cachedPaths.last; if (!_pathsEqual(last.path, oldLast.path) || _paintChanged(last.paint, oldLast.paint)) { return true; } } // 检查临时路径是否变化 if ((tempPath != null && oldDelegate.tempPath == null) || (tempPath == null && oldDelegate.tempPath != null)) { return true; } if (tempPath != null && oldDelegate.tempPath != null && !_pathsEqual(tempPath!, oldDelegate.tempPath!)) { return true; } return false; } bool _pathsEqual(Path a, Path b) { return a.toRawBytes().equals(b.toRawBytes()); } bool _paintChanged(Paint a, Paint b) { return a.color != b.color || a.strokeWidth != b.strokeWidth || a.blendMode != b.blendMode; } }
3. 限制点的采样率,减少冗余点
触摸事件会产生大量密集的点,很多点之间距离极近,完全可以忽略。在触摸事件处理时添加距离判断:
Offset? _lastTouchPoint; // 可根据需求调整阈值,单位dp const double _minPointDistance = 1.5; void _onPanUpdate(DragUpdateDetails details) { final currentOffset = details.localPosition; if (_lastTouchPoint != null) { final distance = (currentOffset - _lastTouchPoint!).distance; if (distance < _minPointDistance) { return; // 忽略过近的点,减少点数量 } } setState(() { points.add(PainterPoint( offset: currentOffset, isEnd: false, color: _currentColor, strokeSize: _currentStrokeSize, isEraser: _isEraser, )); _lastTouchPoint = currentOffset; }); } void _onPanEnd(DragEndDetails details) { setState(() { points.add(PainterPoint(offset: Offset.zero, isEnd: true)); _lastTouchPoint = null; }); }
4. 优化橡皮擦的实现逻辑
用BlendMode.clear生成大量点会导致绘制压力陡增,改用离屏Canvas缓存已绘制内容,擦除时直接在离屏Canvas上绘制擦除形状,避免生成大量擦除点:
// 在Widget中维护离屏Canvas相关对象 PictureRecorder _recorder = PictureRecorder(); Canvas? _offscreenCanvas; Size? _canvasSize; // 初始化/重置离屏Canvas void _initOffscreen(Size size) { if (_canvasSize != size) { _recorder = PictureRecorder(); _offscreenCanvas = Canvas(_recorder, Rect.fromLTWH(0, 0, size.width, size.height)); _canvasSize = size; // 将已有的缓存路径绘制到离屏Canvas for (final cached in _cachedPaths) { _offscreenCanvas!.drawPath(cached.path, cached.paint); } } } // 处理橡皮擦触摸事件 void _handleEraserUpdate(DragUpdateDetails details) { if (_offscreenCanvas == null) return; final eraserPaint = Paint() ..blendMode = BlendMode.clear ..strokeWidth = _currentStrokeSize ..strokeCap = StrokeCap.round; final currentPos = details.localPosition; // 用圆形绘制擦除区域,和画笔粗细匹配 _offscreenCanvas!.drawCircle(currentPos, _currentStrokeSize / 2, eraserPaint); setState(() {}); // 触发重绘 } // 对应的BrushPainter修改为绘制离屏Picture class BrushPainter extends CustomPainter { final Picture? offscreenPicture; final Path? tempDrawingPath; final Paint? tempDrawingPaint; BrushPainter({this.offscreenPicture, this.tempDrawingPath, this.tempDrawingPaint}); @override void paint(Canvas canvas, Size size) { if (offscreenPicture != null) { canvas.drawPicture(offscreenPicture!); } if (tempDrawingPath != null && tempDrawingPaint != null) { canvas.drawPath(tempDrawingPath!, tempDrawingPaint!); } } @override bool shouldRepaint(covariant BrushPainter oldDelegate) { return offscreenPicture != oldDelegate.offscreenPicture || (tempDrawingPath != null && !_pathsEqual(tempDrawingPath!, oldDelegate.tempDrawingPath ?? Path())) || (tempDrawingPaint != null && _paintChanged(tempDrawingPaint!, oldDelegate.tempDrawingPaint ?? Paint())); } bool _pathsEqual(Path a, Path b) { return a.toRawBytes().equals(b.toRawBytes()); } bool _paintChanged(Paint a, Paint b) { return a.color != b.color || a.strokeWidth != b.strokeWidth || a.blendMode != b.blendMode; } }
补充:修复PainterPoint的缺失字段
原代码中BrushPainter用到了currentPoint.isEraser,但PainterPoint类没有这个字段,需要补上:
class PainterPoint { bool isEnd; bool isEraser; // 新增字段 Color color; Offset offset; double strokeSize; BlendMode blendMode; PainterPoint({ required this.offset, required this.isEnd, this.isEraser = false, // 默认不是橡皮擦 this.strokeSize = 2, this.color = Colors.blue, this.blendMode = BlendMode.color, }); }
内容的提问来源于stack exchange,提问作者Arnob
相关产品推荐
相关产品推荐

