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


    演示:

     代码:

    1. import 'dart:ui';
    2. import 'package:flutter/material.dart' hide Image;
    3. import 'package:flutter/services.dart';
    4. import 'package:flutter_screenutil/flutter_screenutil.dart';
    5. import 'package:kq_flutter_widgets/widgets/animate/stack.dart';
    6. import 'package:kq_flutter_widgets/widgets/button/kq_small_button.dart';
    7. import 'package:kq_flutter_widgets/widgets/update/update_view.dart';
    8. ///抠图软件原型
    9. class DrawPathTest extends StatefulWidget {
    10. const DrawPathTest({super.key});
    11. @override
    12. State createState() => DrawPathTestState();
    13. }
    14. class DrawPathTestState extends State<DrawPathTest> {
    15. ///是否绑定左右操作点,即操作一个点,另一个点自动计算
    16. static bool isBind = true;
    17. ///击中范围半径
    18. static double hitRadius = 5;
    19. ///绘制区域state持有
    20. UpdateViewState? state;
    21. ///背景图
    22. Image? _image;
    23. ///历史步骤存储
    24. KqStack stackHistory = KqStack();
    25. ///回收站步骤存储
    26. KqStack stackRecycleBin = KqStack();
    27. ///绘制步骤集合
    28. List drawList = [];
    29. ///手指按下时点击的控制点的位置缓存
    30. Step? hitControlStep;
    31. ///手指按下时点击的画线点的位置缓存
    32. Step? hitDrawStep;
    33. ///闭合绘制完成状态,不再添加点
    34. bool drawFinish = false;
    35. @override
    36. void initState() {
    37. super.initState();
    38. _load("https://c-ssl.duitang.com/uploads/item/201903/19/20190319001325_bjvzi.jpg")
    39. .then((value) {
    40. _image = value;
    41. update();
    42. });
    43. }
    44. @override
    45. Widget build(BuildContext context) {
    46. return Column(
    47. children: [
    48. Expanded(
    49. child: LayoutBuilder(builder: (c, lc) {
    50. return Container(
    51. color: Colors.white60,
    52. child: Listener(
    53. onPointerDown: (v) {
    54. Offset src = v.localPosition;
    55. ///判断是否hit
    56. hitDrawStep = _isHitDrawPoint(src);
    57. if (!drawFinish) {
    58. if (hitDrawStep != null && hitDrawStep!.isFirst) {
    59. _add(src, isLast: true);
    60. drawFinish = true;
    61. } else {
    62. hitControlStep = _isHitControlPoint(src);
    63. hitControlStep ??= _add(src);
    64. }
    65. update();
    66. } else {
    67. hitControlStep = _isHitControlPoint(src);
    68. }
    69. },
    70. onPointerMove: (v) {
    71. if (hitDrawStep != null) {
    72. _update(hitDrawStep!, v.localPosition);
    73. update();
    74. } else if (hitControlStep != null) {
    75. _update(hitControlStep!, v.localPosition);
    76. update();
    77. }
    78. },
    79. child: UpdateView(
    80. build: (UpdateViewState state) {
    81. this.state = state;
    82. return CustomPaint(
    83. size: Size(lc.maxWidth, lc.maxHeight),
    84. painter: TestDraw(_image, drawList),
    85. );
    86. },
    87. ),
    88. ),
    89. );
    90. }),
    91. ),
    92. Row(
    93. children: [
    94. SizedBox(width: 20.r),
    95. Expanded(
    96. child: KqSmallButton(
    97. title: "撤销",
    98. onTap: (disabled) {
    99. _undo();
    100. update();
    101. },
    102. ),
    103. ),
    104. SizedBox(width: 20.r),
    105. Expanded(
    106. child: KqSmallButton(
    107. title: "重做",
    108. onTap: (disabled) {
    109. _redo();
    110. update();
    111. },
    112. ),
    113. ),
    114. SizedBox(width: 20.r),
    115. Expanded(
    116. child: KqSmallButton(
    117. title: "选择",
    118. onTap: (disabled) {
    119. _select();
    120. update();
    121. },
    122. ),
    123. ),
    124. SizedBox(width: 20.r),
    125. Expanded(
    126. child: KqSmallButton(
    127. title: "反选",
    128. onTap: (disabled) {
    129. _invert();
    130. update();
    131. },
    132. ),
    133. ),
    134. SizedBox(width: 20.r),
    135. Expanded(
    136. child: KqSmallButton(
    137. title: "删除",
    138. onTap: (disabled) {
    139. _delete();
    140. update();
    141. },
    142. ),
    143. ),
    144. SizedBox(width: 20.r),
    145. ],
    146. ),
    147. SizedBox(height: 20.r),
    148. ],
    149. );
    150. }
    151. ///更新绘制区域
    152. update() {
    153. state?.update();
    154. }
    155. ///添加点
    156. Step _add(Offset offset, {bool isLast = false}) {
    157. Step step = Step(offset, offset, offset);
    158. step.isLast = isLast;
    159. if (drawList.isEmpty) {
    160. step.isFirst = true;
    161. }
    162. //添加到历史
    163. stackHistory.push(step);
    164. //添加到绘制列表
    165. drawList.add(step);
    166. //清除垃圾箱
    167. stackRecycleBin.clear();
    168. return step;
    169. }
    170. ///判断是否点击在控制点上
    171. Step? _isHitControlPoint(Offset src) {
    172. for (Step step in drawList) {
    173. if (_distance(step.pointRight, src) < hitRadius) {
    174. step.hitPointType = PointType.pointRight;
    175. return step;
    176. } else if (_distance(step.pointLeft, src) < hitRadius) {
    177. step.hitPointType = PointType.pointLeft;
    178. return step;
    179. }
    180. }
    181. return null;
    182. }
    183. ///判断是否点击在连接点上
    184. Step? _isHitDrawPoint(Offset src) {
    185. for (Step step in drawList) {
    186. if (_distance(step.point, src) < hitRadius) {
    187. step.hitPointType = PointType.point;
    188. return step;
    189. }
    190. }
    191. return null;
    192. }
    193. ///更新点信息
    194. _update(Step hitStep, Offset target) {
    195. if (hitStep.hitPointType == PointType.pointRight) {
    196. hitStep.pointRight = target;
    197. if (isBind) {
    198. hitStep.pointLeft = hitStep.point.scale(2, 2) - target;
    199. }
    200. } else if (hitStep.hitPointType == PointType.pointLeft) {
    201. hitStep.pointLeft = target;
    202. if (isBind) {
    203. hitStep.pointRight = hitStep.point.scale(2, 2) - target;
    204. }
    205. } else if (hitStep.hitPointType == PointType.point) {
    206. hitStep.pointLeft = hitStep.pointLeft - hitStep.point + target;
    207. hitStep.pointRight = hitStep.pointRight - hitStep.point + target;
    208. hitStep.point = target;
    209. }
    210. }
    211. ///两点距离
    212. double _distance(Offset one, Offset two) {
    213. return (one - two).distance;
    214. }
    215. ///撤销、后退
    216. _undo() {
    217. Step? step = stackHistory.pop();
    218. if (step != null) {
    219. drawList.remove(step);
    220. stackRecycleBin.push(step);
    221. }
    222. }
    223. ///重做、前进
    224. _redo() {
    225. Step? step = stackRecycleBin.pop();
    226. if (step != null) {
    227. drawList.add(step);
    228. stackHistory.push(step);
    229. }
    230. }
    231. ///选择、完成
    232. _select() {}
    233. ///反选、完成
    234. _invert() {}
    235. ///删除
    236. _delete() {}
    237. ///加载图片
    238. Future _load(String url) async {
    239. ByteData data = await NetworkAssetBundle(Uri.parse(url)).load(url);
    240. Codec codec = await instantiateImageCodec(data.buffer.asUint8List());
    241. FrameInfo fi = await codec.getNextFrame();
    242. return fi.image;
    243. }
    244. }
    245. class TestDraw extends CustomPainter {
    246. static double width = 260;
    247. static double width1 = 50;
    248. static double height1 = 100;
    249. ///绘制集合
    250. final List draw;
    251. ///背景图片
    252. final Image? image;
    253. Step? tempStep;
    254. Step? tempFirstStep;
    255. TestDraw(this.image, this.draw);
    256. @override
    257. void paint(Canvas canvas, Size size) {
    258. ///绘制背景
    259. if (image != null) {
    260. canvas.drawImageRect(
    261. image!,
    262. Rect.fromLTRB(
    263. 0,
    264. 0,
    265. image!.width.toDouble(),
    266. image!.height.toDouble(),
    267. ),
    268. Rect.fromLTRB(
    269. width1,
    270. height1,
    271. width + width1,
    272. width * image!.height / image!.width + height1,
    273. ),
    274. Paint(),
    275. );
    276. }
    277. if (draw.isNotEmpty) {
    278. ///构建画点与点之间的连线的path
    279. Path path = Path();
    280. ///绘制点和线
    281. for (int i = 0; i < draw.length; i++) {
    282. Step step = draw[i];
    283. if (!step.isLast) {
    284. canvas.drawCircle(step.point, 4.r, Paint()..color = Colors.red);
    285. canvas.drawCircle(
    286. step.pointLeft, 4.r, Paint()..color = Colors.purple);
    287. canvas.drawCircle(
    288. step.pointRight, 4.r, Paint()..color = Colors.purple);
    289. ///画控制点和连线点之间的线段
    290. canvas.drawLine(
    291. step.point,
    292. step.pointLeft,
    293. Paint()
    294. ..color = Colors.green
    295. ..style = PaintingStyle.stroke);
    296. canvas.drawLine(
    297. step.point,
    298. step.pointRight,
    299. Paint()
    300. ..color = Colors.green
    301. ..style = PaintingStyle.stroke);
    302. }
    303. ///构建画点与点之间的连线的path
    304. if (step.isLast) {
    305. if (tempFirstStep != null && tempStep != null) {
    306. path.cubicTo(
    307. tempStep!.pointRight.dx,
    308. tempStep!.pointRight.dy,
    309. tempFirstStep!.pointLeft.dx,
    310. tempFirstStep!.pointLeft.dy,
    311. tempFirstStep!.point.dx,
    312. tempFirstStep!.point.dy,
    313. );
    314. }
    315. } else {
    316. //处理初始点
    317. if (step.isFirst) {
    318. tempFirstStep = step;
    319. path.moveTo(step.point.dx, step.point.dy);
    320. }
    321. if (tempStep != null) {
    322. path.cubicTo(
    323. tempStep!.pointRight.dx,
    324. tempStep!.pointRight.dy,
    325. step.pointLeft.dx,
    326. step.pointLeft.dy,
    327. step.point.dx,
    328. step.point.dy,
    329. );
    330. }
    331. }
    332. tempStep = step;
    333. }
    334. if (draw.length >= 2) {
    335. canvas.drawPath(
    336. path,
    337. Paint()
    338. ..color = Colors.red
    339. ..style = PaintingStyle.stroke
    340. ..strokeWidth = 1.5,
    341. );
    342. }
    343. }
    344. }
    345. @override
    346. bool shouldRepaint(covariant CustomPainter oldDelegate) {
    347. return true;
    348. }
    349. }
    350. class Step {
    351. ///线条连接点
    352. Offset point;
    353. ///右控制点
    354. Offset pointRight;
    355. ///左控制点(起始点没有左控制点的)
    356. Offset pointLeft;
    357. ///是否选中了点的类型
    358. PointType hitPointType = PointType.pointRight;
    359. ///是否是第一个控制点
    360. bool isFirst = false;
    361. ///是否是最后一个控制点
    362. bool isLast = false;
    363. Step(
    364. this.point,
    365. this.pointRight,
    366. this.pointLeft,
    367. );
    368. }
    369. ///点类型
    370. enum PointType {
    371. ///线条连接点
    372. point,
    373. ///右控制点
    374. pointRight,
    375. ///左控制点
    376. pointLeft
    377. }

    stack代码:

    1. ///栈,先进后出
    2. class KqStack<T> {
    3. final List _stack = [];
    4. ///入栈
    5. push(T obj) {
    6. _stack.add(obj);
    7. }
    8. ///出栈
    9. T? pop() {
    10. if (_stack.isEmpty) {
    11. return null;
    12. } else {
    13. return _stack.removeLast();
    14. }
    15. }
    16. ///栈长度
    17. length() {
    18. return _stack.length;
    19. }
    20. ///清除栈
    21. clear() {
    22. _stack.clear();
    23. }
    24. }

    主要思路:

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

  • 相关阅读:
    数字孪生实际应用:智慧城市项目建设解决方案
    如何查看yandex的转化Session Replay(会话重播)
    pandas读取csv文件(分隔符是一个或者多个空格)
    【产品设计】APP提升用户注册率的五个方案探讨结论
    [paper]PYVA 论文浅析
    Spring Boot 3.0:构建下一代Java应用的新方法
    数据结构-自学-自用
    算法训练|交易逆序对的总数、验证二叉搜索树的后序遍历
    AYIT嵌入式实验室2023级C语言训练1-4章训练题
    零时科技 || 分布式资本创始人4200万美金资产被盗分析及追踪工作
  • 原文地址:https://blog.csdn.net/u012800952/article/details/133138079