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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 14:24:54