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

Flutter CustomPainter:如何无需白色绘制实现擦除功能

解决Flutter背景图上绘图的擦除问题

核心思路是图层分离+混合模式:把绘制内容放在单独图层,擦除时使用BlendMode.clear清除绘制图层的像素,而非用白色覆盖背景。以下是具体实现步骤:

1. 修复并扩展DrawingPoints类

增加擦除标记字段,同时修正原代码中==运算符的逻辑错误:

class DrawingPoints {
  final Offset points;
  final Paint paint;
  final bool isEraser;

  const DrawingPoints({required this.points, required this.paint, this.isEraser = false});

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is DrawingPoints &&
        other.points == points &&
        other.paint.color == paint.color &&
        other.isEraser == isEraser;
  }

  @override
  int get hashCode => points.hashCode ^ paint.hashCode ^ isEraser.hashCode;
}

2. 主页面添加擦除模式与触摸事件处理

新增擦除状态变量,在触摸时生成对应模式的Paint(擦除模式启用BlendMode.clear),同时将背景图与绘制层分离:

// 新增擦除状态控制变量
bool _isErasing = false;

// 页面核心组件
Column(
  children: [
    // 切换擦除/绘制模式的按钮
    IconButton(
      icon: Icon(_isErasing ? Icons.brush : Icons.delete_outline),
      onPressed: () => setState(() => _isErasing = !_isErasing),
    ),
    Expanded(
      child: Listener(
        onPointerDown: (details) {
          final renderBox = context.findRenderObject() as RenderBox?;
          if (renderBox == null) return;

          final localPos = renderBox.globalToLocal(details.position);
          final paint = _isErasing
              ? Paint()
                ..strokeCap = StrokeCap.round
                ..isAntiAlias = true
                ..blendMode = BlendMode.clear // 核心:清除绘制层像素
                ..strokeWidth = strokeWidth * 2 // 放大擦除笔刷,适配儿童操作
              : Paint()
                ..strokeCap = StrokeCap.round
                ..isAntiAlias = true
                ..color = widget.selectedColor.withOpacity(1.0)
                ..strokeWidth = strokeWidth;

          setState(() {
            points.add(DrawingPoints(
              points: localPos,
              paint: paint,
              isEraser: _isErasing,
            ));
          });
        },
        onPointerMove: (details) {
          final renderBox = context.findRenderObject() as RenderBox?;
          if (renderBox == null) return;

          final localPos = renderBox.globalToLocal(details.position);
          final paint = _isErasing
              ? Paint()
                ..strokeCap = StrokeCap.round
                ..isAntiAlias = true
                ..blendMode = BlendMode.clear
                ..strokeWidth = strokeWidth * 2
              : Paint()
                ..strokeCap = StrokeCap.round
                ..isAntiAlias = true
                ..color = widget.selectedColor.withOpacity(1.0)
                ..strokeWidth = strokeWidth;

          setState(() {
            points.add(DrawingPoints(
              points: localPos,
              paint: paint,
              isEraser: _isErasing,
            ));
          });
        },
        onPointerUp: (details) => setState(() => points.add(null)),
        child: Stack(
          fit: StackFit.expand,
          children: [
            // 替换为你的背景图片组件
            Image.asset('assets/background.jpg', fit: BoxFit.cover),
            CustomPaint(
              size: Size.infinite,
              painter: DrawingPainter(pointsList: points),
            ),
          ],
        ),
      ),
    ),
  ],
)

3. 优化CustomPainter绘制逻辑

使用离屏图层(saveLayer)隔离绘制内容,确保擦除操作仅影响绘制层,不干扰背景:

class DrawingPainter extends CustomPainter {
  final List<DrawingPoints?> pointsList;
  final List<Offset> offsetPoints = [];

  DrawingPainter({required this.pointsList});

  @override
  void paint(Canvas canvas, Size size) {
    // 创建独立绘制图层,隔离背景与绘制内容
    final rect = Offset.zero & size;
    canvas.saveLayer(rect, Paint());

    for (int i = 0; i < pointsList.length - 1; i++) {
      if (pointsList[i] != null && pointsList[i + 1] != null) {
        canvas.drawLine(
          pointsList[i]!.points,
          pointsList[i + 1]!.points,
          pointsList[i]!.paint,
        );
      } else if (pointsList[i] != null && pointsList[i + 1] == null) {
        offsetPoints
          ..clear()
          ..add(pointsList[i]!.points)
          ..add(Offset(pointsList[i]!.points.dx + 0.1, pointsList[i]!.points.dy + 0.1));
        canvas.drawPoints(PointMode.points, offsetPoints, pointsList[i]!.paint);
      }
    }

    canvas.restore(); // 将绘制图层合并到主画布
  }

  @override
  bool shouldRepaint(DrawingPainter oldDelegate) {
    return oldDelegate.pointsList != pointsList;
  }
}

关键说明

  • BlendMode.clear:该模式会清除当前图层对应区域的像素,直接露出下方背景图,完美实现擦除效果,不会覆盖背景。
  • 离屏图层:通过saveLayer将绘制内容与背景完全隔离,避免擦除操作影响背景,同时优化绘制性能。
  • 儿童友好调整:放大擦除笔刷尺寸,降低操作难度。

内容的提问来源于stack exchange,提问作者Loïc Yabili

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 09:27:03