如何在基于手势绘制的CustomPainter中维持60fps帧率
嘿,我仔细看了你的签名绘制实现,帧率掉到5fps确实挺闹心的,咱们从几个关键方向来优化,应该能大幅提升性能:
1. 复用Paint对象,避免频繁创建
你当前的代码大概率是在paint方法里每次都新建Paint实例,这会触发频繁的垃圾回收(GC),拖慢帧率。应该把Paint作为自定义Painter类的成员变量初始化好,全程复用:
class SignaturePainter extends CustomPainter { final Path drawPath; final Paint _paint; // 初始化时就创建Paint,后续不再重复创建 SignaturePainter(this.drawPath) : _paint = Paint() ..color = Colors.black ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round ..isAntiAlias = true; @override void paint(Canvas canvas, Size size) { canvas.drawPath(drawPath, _paint); // 直接复用已有的Paint } // ... 其他方法 }
2. 优化路径构建,减少重复计算
如果你的代码每次重绘都循环所有点来构建Path,那点越多开销越大。建议维护一个全局的Path对象,在手势回调里直接更新Path,而不是每次paint都重新构建:
// 用ChangeNotifier管理绘制状态,只在点新增时更新Path class SignatureController extends ChangeNotifier { final Path _drawPath = Path(); final Paint _paint = Paint() ..color = Colors.black ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round ..isAntiAlias = true; Path get drawPath => _drawPath; Paint get paint => _paint; void startDrawing(Offset startPoint) { _drawPath.moveTo(startPoint.dx, startPoint.dy); notifyListeners(); } void updateDrawing(Offset newPoint) { _drawPath.lineTo(newPoint.dx, newPoint.dy); notifyListeners(); } } // Painter直接复用已构建好的Path class SignaturePainter extends CustomPainter { final SignatureController controller; SignaturePainter(this.controller) : super(repaint: controller); @override void paint(Canvas canvas, Size size) { canvas.drawPath(controller.drawPath, controller.paint); } @override bool shouldRepaint(SignaturePainter oldDelegate) => false; }
3. 简化路径点数量,去掉冗余点
用户平移时会产生大量密集的点,但很多点是冗余的。用Ramer-Douglas-Peucker算法可以在不影响视觉效果的前提下,大幅减少路径上的点数量:
// 实现路径简化算法 List<Offset> simplifyPath(List<Offset> points, double epsilon) { if (points.length <= 2) return points; double maxDist = 0; int splitIndex = 0; final start = points.first; final end = points.last; for (int i = 1; i < points.length - 1; i++) { final dist = _perpendicularDistance(points[i], start, end); if (dist > maxDist) { maxDist = dist; splitIndex = i; } } if (maxDist > epsilon) { final left = simplifyPath(points.sublist(0, splitIndex + 1), epsilon); final right = simplifyPath(points.sublist(splitIndex), epsilon); return left.sublist(0, left.length - 1)..addAll(right); } else { return [start, end]; } } // 计算点到线段的垂直距离 double _perpendicularDistance(Offset point, Offset lineStart, Offset lineEnd) { final dx = lineEnd.dx - lineStart.dx; final dy = lineEnd.dy - lineStart.dy; if (dx == 0 && dy == 0) return (point - lineStart).distance; final t = ((point.dx - lineStart.dx) * dx + (point.dy - lineStart.dy) * dy) / (dx*dx + dy*dy); final projection = Offset(lineStart.dx + t*dx, lineStart.dy + t*dy); return (point - projection).distance; }
在手势回调里收集点后,调用simplifyPath(points, 1.0)(epsilon值可以根据需求调整),再把简化后的点添加到Path中。
4. 缓存已完成的绘制内容,避免重复绘制
当用户完成一段手势后,把这部分内容缓存成Picture对象,后续重绘时只需要绘制缓存的Picture和新的手势路径,不用每次都重新画所有内容:
class SignatureController extends ChangeNotifier { Path _currentPath = Path(); Picture? _completedPicture; final Paint _paint = Paint()..color = Colors.black..strokeWidth = 2.0..strokeCap = StrokeCap.round; void startDrawing(Offset point) { _currentPath.moveTo(point.dx, point.dy); notifyListeners(); } void updateDrawing(Offset point) { _currentPath.lineTo(point.dx, point.dy); notifyListeners(); } void endDrawing(Size canvasSize) { // 把当前路径记录到已完成的Picture中 final recorder = PictureRecorder(); final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height)); if (_completedPicture != null) { canvas.drawPicture(_completedPicture!); } canvas.drawPath(_currentPath, _paint); _completedPicture = recorder.endRecording(); _currentPath = Path(); // 重置当前路径 notifyListeners(); } // 提供给Painter的绘制方法 void draw(Canvas canvas, Size size) { if (_completedPicture != null) { canvas.drawPicture(_completedPicture!); } canvas.drawPath(_currentPath, _paint); } } class SignaturePainter extends CustomPainter { final SignatureController controller; SignaturePainter(this.controller) : super(repaint: controller); @override void paint(Canvas canvas, Size size) { controller.draw(canvas, size); } @override bool shouldRepaint(SignaturePainter oldDelegate) => false; }
5. 正确实现shouldRepaint,避免不必要的重绘
确保shouldRepaint只在绘制内容真的变化时返回true,不要默认返回true。比如对比新旧Delegate的路径或缓存内容是否不同:
@override bool shouldRepaint(SignaturePainter oldDelegate) { return oldDelegate.controller._completedPicture != controller._completedPicture || !_pathsAreEqual(oldDelegate.controller._currentPath, controller._currentPath); } // 辅助方法:比较两个Path是否相等(简单实现,复杂场景可以用更精确的方式) bool _pathsAreEqual(Path a, Path b) { final aBytes = a.toBytes(); final bBytes = b.toBytes(); return listEquals(aBytes, bBytes); }
按照这些方法优化后,帧率应该能回到接近60fps的水平,尤其是绘制复杂内容时的提升会很明显。
内容的提问来源于stack exchange,提问作者user983327
相关产品推荐
相关产品推荐

