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

Flutter绘图应用橡皮擦无指针动态反馈问题求助

解决Flutter分层绘图中橡皮擦实时轨迹不显示的问题

问题分析

你遇到的核心矛盾是:使用canvas.saveLayer+BlendMode.clear+canvas.restore能实现仅擦除当前图层,但实时绘制时看不到指针轨迹;去掉这组操作后轨迹可见,但橡皮擦会穿透所有底层图层。

原因在于saveLayer会创建临时缓冲图层,所有绘制操作先在缓冲中进行,直到restore才会合并到主画布。实时移动时,每次paint的擦除操作被限制在缓冲层,未及时合并到主画布,导致轨迹无法实时显示。同时原代码中paint方法使用异步修饰符是错误的——CustomPainter.paint必须同步执行,异步会引发渲染异常。

解决方案

采用「实时预览层+正式图层提交」的分离方案:

  1. 指针移动时,在独立的临时预览层绘制橡皮擦轨迹,不使用saveLayer,保证实时可见。
  2. 指针抬起后,将预览轨迹用saveLayer+restore的方式应用到目标图层,确保仅擦除当前图层。

修改后的代码

1. 调整自定义Painter

添加预览模式参数,区分实时预览和正式绘制逻辑:

class SingleLinePainter extends CustomPainter {
  final Stroke? line;
  final StrokeOptions options;
  Color currentColor = Colors.black;
  final bool isPreview;

  SingleLinePainter({
    required this.currentColor,
    required this.line,
    required this.options,
    this.isPreview = false,
  });

  void setColorForLayer(Color color) {
    currentColor = color;
  }

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    var _blendMode = BlendMode.srcOver;

    if (line!.isEraser) {
      // 正式绘制时才启用saveLayer,预览模式跳过
      if (!isPreview) {
        canvas.saveLayer(null, Paint());
      }
      _blendMode = BlendMode.clear;
    }
    paint.blendMode = _blendMode;
    paint.color = currentColor;

    final outlinePoints = getStroke(
      line!.points,
      size: options.size,
      thinning: options.thinning,
      smoothing: options.smoothing,
      streamline: options.streamline,
      taperStart: options.taperStart,
      capStart: options.capStart,
      taperEnd: options.taperEnd,
      capEnd: options.capEnd,
      simulatePressure: options.simulatePressure,
      isComplete: options.isComplete,
    );

    final path = Path();

    if (outlinePoints.isEmpty) {
      return;
    } else if (outlinePoints.length < 2) {
      path.addOval(Rect.fromCircle(
        center: Offset(outlinePoints[0].x, outlinePoints[0].y), 
        radius: 1,
      ));
    } else {
      path.moveTo(outlinePoints[0].x, outlinePoints[0].y);
      for (int i = 1; i < outlinePoints.length - 1; ++i) {
        final p0 = outlinePoints[i];
        final p1 = outlinePoints[i + 1];
        path.quadraticBezierTo(
          p0.x, 
          p0.y, 
          (p0.x + p1.x) / 2, 
          (p0.y + p1.y) / 2,
        );
      }
    }

    canvas.drawPath(path, paint);
    if (line!.isEraser && !isPreview) {
      canvas.restore();
    }
  }

  @override
  bool shouldRepaint(SingleLinePainter oldDelegate) {
    return true;
  }
}

2. 更新路径构建Widget

添加临时预览轨迹的维护和绘制:

// 新增临时预览轨迹变量
Stroke? _previewEraserStroke;

Widget buildCurrentPath(BuildContext context) {
  return Listener(
    onPointerDown: onPointerDown,
    onPointerMove: onPointerMove,
    onPointerUp: onPointerUp,
    child: RepaintBoundary(
      child: Stack(
        children: [
          // 正式图层绘制区域
          Container(
            color: Colors.transparent,
            width: widget.width,
            height: widget.height,
            child: StreamBuilder<Stroke>(
              stream: currentLineStreamController.stream,
              builder: (context, snapshot) {
                return CustomPaint(
                  painter: SingleLinePainter(
                    currentColor: pickerColor,
                    line: line == null ? null : line!,
                    options: options,
                  ),
                );
              },
            ),
          ),
          // 橡皮擦实时预览层
          if (_previewEraserStroke != null)
            CustomPaint(
              size: Size(widget.width, widget.height),
              painter: SingleLinePainter(
                currentColor: pickerColor,
                line: _previewEraserStroke!,
                options: options.copyWith(isPreview: true),
                isPreview: true,
              ),
            ),
        ],
      ),
    ),
  );
}

// 更新指针回调逻辑
void onPointerDown(PointerDownEvent event) {
  if (/* 此处判断是否处于橡皮擦模式 */ true) {
    _previewEraserStroke = Stroke(isEraser: true, points: [event.localPosition]);
    setState(() {});
  } else {
    // 原有普通绘制逻辑
  }
}

void onPointerMove(PointerMoveEvent event) {
  if (_previewEraserStroke != null) {
    _previewEraserStroke!.points.add(event.localPosition);
    setState(() {});
  } else {
    // 原有普通绘制逻辑
  }
}

void onPointerUp(PointerUpEvent event) {
  if (_previewEraserStroke != null) {
    // 将预览轨迹提交到正式图层处理
    _applyEraserToCurrentLayer(_previewEraserStroke!);
    _previewEraserStroke = null;
    setState(() {});
  } else {
    // 原有普通绘制逻辑
  }
}

// 实现正式图层的橡皮擦应用逻辑
void _applyEraserToCurrentLayer(Stroke eraserStroke) {
  // 示例:将橡皮擦轨迹添加到当前图层的Stroke列表,触发正式绘制
  // currentLayerStrokes.add(eraserStroke);
  // currentLineStreamController.add(eraserStroke);
}

3. 补充StrokeOptions的copyWith方法

确保能传递预览模式参数:

class StrokeOptions {
  // 原有字段...
  final bool isPreview;

  StrokeOptions({
    // 原有参数...
    this.isPreview = false,
  });

  StrokeOptions copyWith({
    // 原有参数...
    bool? isPreview,
  }) {
    return StrokeOptions(
      // 原有参数赋值...
      isPreview: isPreview ?? this.isPreview,
    );
  }
}

方案说明

  • 实时预览层单独绘制橡皮擦轨迹,不使用saveLayer,因此轨迹能实时显示,且不会修改正式图层。
  • 指针抬起后,将预览轨迹提交到正式图层,用saveLayer+restore保证仅擦除当前图层,避免穿透底层。
  • 移除了paint方法的异步修饰符,修复了潜在的渲染异常问题。

内容的提问来源于stack exchange,提问作者Iryna

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 06:38:12