You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在基于手势绘制的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 04:06:05