Flutter实现PS钢笔工具,实现高精度抠图的效果。

演示:

代码:

Dart 复制代码
import 'dart:ui';

import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kq_flutter_widgets/widgets/animate/stack.dart';
import 'package:kq_flutter_widgets/widgets/button/kq_small_button.dart';
import 'package:kq_flutter_widgets/widgets/update/update_view.dart';

///抠图软件原型
class DrawPathTest extends StatefulWidget {
  const DrawPathTest({super.key});

  @override
  State<StatefulWidget> createState() => DrawPathTestState();
}

class DrawPathTestState extends State<DrawPathTest> {
  ///是否绑定左右操作点,即操作一个点,另一个点自动计算
  static bool isBind = true;

  ///击中范围半径
  static double hitRadius = 5;

  ///绘制区域state持有
  UpdateViewState? state;

  ///背景图
  Image? _image;

  ///历史步骤存储
  KqStack stackHistory = KqStack();

  ///回收站步骤存储
  KqStack stackRecycleBin = KqStack();

  ///绘制步骤集合
  List<Step> drawList = [];

  ///手指按下时点击的控制点的位置缓存
  Step? hitControlStep;

  ///手指按下时点击的画线点的位置缓存
  Step? hitDrawStep;

  ///闭合绘制完成状态,不再添加点
  bool drawFinish = false;

  @override
  void initState() {
    super.initState();
    _load("https://c-ssl.duitang.com/uploads/item/201903/19/20190319001325_bjvzi.jpg")
        .then((value) {
      _image = value;
      update();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: LayoutBuilder(builder: (c, lc) {
            return Container(
              color: Colors.white60,
              child: Listener(
                onPointerDown: (v) {
                  Offset src = v.localPosition;

                  ///判断是否hit
                  hitDrawStep = _isHitDrawPoint(src);
                  if (!drawFinish) {
                    if (hitDrawStep != null && hitDrawStep!.isFirst) {
                      _add(src, isLast: true);
                      drawFinish = true;
                    } else {
                      hitControlStep = _isHitControlPoint(src);
                      hitControlStep ??= _add(src);
                    }
                    update();
                  } else {
                    hitControlStep = _isHitControlPoint(src);
                  }
                },
                onPointerMove: (v) {
                  if (hitDrawStep != null) {
                    _update(hitDrawStep!, v.localPosition);
                    update();
                  } else if (hitControlStep != null) {
                    _update(hitControlStep!, v.localPosition);
                    update();
                  }
                },
                child: UpdateView(
                  build: (UpdateViewState state) {
                    this.state = state;
                    return CustomPaint(
                      size: Size(lc.maxWidth, lc.maxHeight),
                      painter: TestDraw(_image, drawList),
                    );
                  },
                ),
              ),
            );
          }),
        ),
        Row(
          children: [
            SizedBox(width: 20.r),
            Expanded(
              child: KqSmallButton(
                title: "撤销",
                onTap: (disabled) {
                  _undo();
                  update();
                },
              ),
            ),
            SizedBox(width: 20.r),
            Expanded(
              child: KqSmallButton(
                title: "重做",
                onTap: (disabled) {
                  _redo();
                  update();
                },
              ),
            ),
            SizedBox(width: 20.r),
            Expanded(
              child: KqSmallButton(
                title: "选择",
                onTap: (disabled) {
                  _select();
                  update();
                },
              ),
            ),
            SizedBox(width: 20.r),
            Expanded(
              child: KqSmallButton(
                title: "反选",
                onTap: (disabled) {
                  _invert();
                  update();
                },
              ),
            ),
            SizedBox(width: 20.r),
            Expanded(
              child: KqSmallButton(
                title: "删除",
                onTap: (disabled) {
                  _delete();
                  update();
                },
              ),
            ),
            SizedBox(width: 20.r),
          ],
        ),
        SizedBox(height: 20.r),
      ],
    );
  }

  ///更新绘制区域
  update() {
    state?.update();
  }

  ///添加点
  Step _add(Offset offset, {bool isLast = false}) {
    Step step = Step(offset, offset, offset);
    step.isLast = isLast;
    if (drawList.isEmpty) {
      step.isFirst = true;
    }
    //添加到历史
    stackHistory.push(step);
    //添加到绘制列表
    drawList.add(step);
    //清除垃圾箱
    stackRecycleBin.clear();
    return step;
  }

  ///判断是否点击在控制点上
  Step? _isHitControlPoint(Offset src) {
    for (Step step in drawList) {
      if (_distance(step.pointRight, src) < hitRadius) {
        step.hitPointType = PointType.pointRight;
        return step;
      } else if (_distance(step.pointLeft, src) < hitRadius) {
        step.hitPointType = PointType.pointLeft;
        return step;
      }
    }
    return null;
  }

  ///判断是否点击在连接点上
  Step? _isHitDrawPoint(Offset src) {
    for (Step step in drawList) {
      if (_distance(step.point, src) < hitRadius) {
        step.hitPointType = PointType.point;
        return step;
      }
    }
    return null;
  }

  ///更新点信息
  _update(Step hitStep, Offset target) {
    if (hitStep.hitPointType == PointType.pointRight) {
      hitStep.pointRight = target;
      if (isBind) {
        hitStep.pointLeft = hitStep.point.scale(2, 2) - target;
      }
    } else if (hitStep.hitPointType == PointType.pointLeft) {
      hitStep.pointLeft = target;
      if (isBind) {
        hitStep.pointRight = hitStep.point.scale(2, 2) - target;
      }
    } else if (hitStep.hitPointType == PointType.point) {
      hitStep.pointLeft = hitStep.pointLeft - hitStep.point + target;
      hitStep.pointRight = hitStep.pointRight - hitStep.point + target;
      hitStep.point = target;
    }
  }

  ///两点距离
  double _distance(Offset one, Offset two) {
    return (one - two).distance;
  }

  ///撤销、后退
  _undo() {
    Step? step = stackHistory.pop();
    if (step != null) {
      drawList.remove(step);
      stackRecycleBin.push(step);
    }
  }

  ///重做、前进
  _redo() {
    Step? step = stackRecycleBin.pop();
    if (step != null) {
      drawList.add(step);
      stackHistory.push(step);
    }
  }

  ///选择、完成
  _select() {}

  ///反选、完成
  _invert() {}

  ///删除
  _delete() {}

  ///加载图片
  Future<Image> _load(String url) async {
    ByteData data = await NetworkAssetBundle(Uri.parse(url)).load(url);
    Codec codec = await instantiateImageCodec(data.buffer.asUint8List());
    FrameInfo fi = await codec.getNextFrame();
    return fi.image;
  }
}

class TestDraw extends CustomPainter {
  static double width = 260;
  static double width1 = 50;
  static double height1 = 100;

  ///绘制集合
  final List<Step> draw;

  ///背景图片
  final Image? image;

  Step? tempStep;
  Step? tempFirstStep;

  TestDraw(this.image, this.draw);

  @override
  void paint(Canvas canvas, Size size) {
    ///绘制背景
    if (image != null) {
      canvas.drawImageRect(
        image!,
        Rect.fromLTRB(
          0,
          0,
          image!.width.toDouble(),
          image!.height.toDouble(),
        ),
        Rect.fromLTRB(
          width1,
          height1,
          width + width1,
          width * image!.height / image!.width + height1,
        ),
        Paint(),
      );
    }

    if (draw.isNotEmpty) {
      ///构建画点与点之间的连线的path
      Path path = Path();

      ///绘制点和线
      for (int i = 0; i < draw.length; i++) {
        Step step = draw[i];
        if (!step.isLast) {
          canvas.drawCircle(step.point, 4.r, Paint()..color = Colors.red);
          canvas.drawCircle(
              step.pointLeft, 4.r, Paint()..color = Colors.purple);
          canvas.drawCircle(
              step.pointRight, 4.r, Paint()..color = Colors.purple);

          ///画控制点和连线点之间的线段
          canvas.drawLine(
              step.point,
              step.pointLeft,
              Paint()
                ..color = Colors.green
                ..style = PaintingStyle.stroke);
          canvas.drawLine(
              step.point,
              step.pointRight,
              Paint()
                ..color = Colors.green
                ..style = PaintingStyle.stroke);
        }

        ///构建画点与点之间的连线的path
        if (step.isLast) {
          if (tempFirstStep != null && tempStep != null) {
            path.cubicTo(
              tempStep!.pointRight.dx,
              tempStep!.pointRight.dy,
              tempFirstStep!.pointLeft.dx,
              tempFirstStep!.pointLeft.dy,
              tempFirstStep!.point.dx,
              tempFirstStep!.point.dy,
            );
          }
        } else {
          //处理初始点
          if (step.isFirst) {
            tempFirstStep = step;
            path.moveTo(step.point.dx, step.point.dy);
          }
          if (tempStep != null) {
            path.cubicTo(
              tempStep!.pointRight.dx,
              tempStep!.pointRight.dy,
              step.pointLeft.dx,
              step.pointLeft.dy,
              step.point.dx,
              step.point.dy,
            );
          }
        }

        tempStep = step;
      }

      if (draw.length >= 2) {
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.red
            ..style = PaintingStyle.stroke
            ..strokeWidth = 1.5,
        );
      }
    }
  }

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

class Step {
  ///线条连接点
  Offset point;

  ///右控制点
  Offset pointRight;

  ///左控制点(起始点没有左控制点的)
  Offset pointLeft;

  ///是否选中了点的类型
  PointType hitPointType = PointType.pointRight;

  ///是否是第一个控制点
  bool isFirst = false;

  ///是否是最后一个控制点
  bool isLast = false;

  Step(
    this.point,
    this.pointRight,
    this.pointLeft,
  );
}

///点类型
enum PointType {
  ///线条连接点
  point,

  ///右控制点
  pointRight,

  ///左控制点
  pointLeft
}

stack代码:

Dart 复制代码
///栈,先进后出
class KqStack<T> {
  final List<T> _stack = [];

  ///入栈
  push(T obj) {
    _stack.add(obj);
  }

  ///出栈
  T? pop() {
    if (_stack.isEmpty) {
      return null;
    } else {
      return _stack.removeLast();
    }
  }

  ///栈长度
  length() {
    return _stack.length;
  }

  ///清除栈
  clear() {
    _stack.clear();
  }
}

主要思路:

更具手指点击屏幕的位置,记录点击的位置,并生成绘制点和两个控制点,手指拖动控制点时,动态刷新控制点位置,然后利用flutter绘制机制,在canvas上根据点的位置和控制点的位置绘制三阶贝塞尔曲线,实现钢笔工具效果。具体实现可以看代码,有注释,逻辑应该还算清晰。

相关推荐
于慨2 小时前
flutter基础组件用法
开发语言·javascript·flutter
恋猫de小郭5 小时前
Android CLI ,谷歌为 Android 开发者专研的 AI Agent,提速三倍
android·前端·flutter
火柴就是我6 小时前
flutter pushAndRemoveUntil 的一次小疑惑
flutter
于慨6 小时前
flutter doctor问题解决
flutter
唔666 小时前
flutter 图片加载类 图片的安全使用
安全·flutter
Nathan202406167 小时前
Flutter - InheritedWidget
flutter·dart
恋猫de小郭8 小时前
JetBrains Amper 0.10 ,期待它未来替代 Gradle
android·前端·flutter
Lanren的编程日记9 小时前
Flutter鸿蒙应用开发:实时聊天功能集成实战
flutter·华为·harmonyos
Utopia^18 小时前
鸿蒙flutter第三方库适配 - 联系人备份工具
flutter·华为·harmonyos
念格1 天前
Flutter 仿微信输入框最佳实践:自适应高度 + 超行数智能切换全屏
前端·flutter