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浮动按钮在OpenHarmony平台的实践经验
flutter·harmonyos·鸿蒙
程序员老刘5 小时前
一杯奶茶钱,PicGo + 阿里云 OSS 搭建永久稳定的个人图床
flutter·markdown
奋斗的小青年!!9 小时前
OpenHarmony Flutter 拖拽排序组件性能优化与跨平台适配指南
flutter·harmonyos·鸿蒙
小雨下雨的雨10 小时前
Flutter 框架跨平台鸿蒙开发 —— Stack 控件之三维层叠艺术
flutter·华为·harmonyos
行者9611 小时前
OpenHarmony平台Flutter手风琴菜单组件的跨平台适配实践
flutter·harmonyos·鸿蒙
小雨下雨的雨12 小时前
Flutter 框架跨平台鸿蒙开发 —— Flex 控件之响应式弹性布局
flutter·ui·华为·harmonyos·鸿蒙系统
cn_mengbei12 小时前
Flutter for OpenHarmony 实战:CheckboxListTile 复选框列表项详解
flutter
cn_mengbei13 小时前
Flutter for OpenHarmony 实战:Switch 开关按钮详解
flutter
奋斗的小青年!!13 小时前
OpenHarmony Flutter实战:打造高性能订单确认流程步骤条
flutter·harmonyos·鸿蒙
Coder_Boy_13 小时前
Flutter基础介绍-跨平台移动应用开发框架
spring boot·flutter