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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 00:54:58