设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间23 现在时间23 缓存数据 好好的睡一觉吧,闭上眼睛做个好梦,明天睁眼又会是美好的一天,晚安好梦。

好好的睡一觉吧,闭上眼睛做个好梦,明天睁眼又会是美好的一天,晚安好梦。

查看: 974|回复: 2

Flutter实现给图片添加涂鸦功能

[复制链接]

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
31
主题
27
精华
0
金钱
96
积分
60
注册时间
2023-9-29
最后登录
2025-5-31

发表于 2024-6-14 00:24:11 | 显示全部楼层 |阅读模式
目录


  • 简介

    • 关闭和确定
    • 颜色选择
    • 撤销功能
    • 清除功能
    • 涂鸦图片的放大和缩小
    • 放大缩小后按照新的线条粗细继续涂鸦
    • 保存涂鸦图片到本地。

  • 代码介绍

    • 涂鸦颜色选择组件。
    • 存储颜色和食指划过点的数据类
    • 具体涂鸦点的绘制
    • 涂鸦主要界面代码


简介

先来张图,看一下最终效果
1.webp


关闭和确定


  • 关闭和确定功能对应的是界面左下角叉号,和右下角对钩,关闭按钮仅做取消当次涂鸦,读者可自行设置点击后功能,也可自行更改相应UI。选择功能点击后会执行一段把当前涂鸦后的图片合成并保存到本地的操作。具体请看示例代码。

颜色选择


  • 颜色选择功能可选择和标识当前涂鸦颜色,和指示当前涂鸦颜色的选中状态(以白色外圈标识)。切换颜色后下一次涂鸦即会使用新的颜色。

撤销功能


  • 撤销功能可撤销最近的一次涂鸦。如没有涂鸦时显示置灰的撤销按钮。

清除功能


  • 清除功能可清除所有涂鸦,如当前没有任何涂鸦时显示置灰的清除按钮。

涂鸦图片的放大和缩小


  • 可双指滑动切换涂鸦放大缩小的效果。

放大缩小后按照新的线条粗细继续涂鸦


  • 涂鸦放大或缩小后,涂鸦线条会随之放大和缩小,此时如果继续涂鸦,则新涂鸦显示的粗细程度与放大或缩小后的线条粗细程度保持一致。

保存涂鸦图片到本地。


  • flutter涂鸦后的图片可合成新图片并保存到本地路径。

代码介绍


涂鸦颜色选择组件。
  1. 主要是显示为可配置的圆点和外圈
复制代码
circle_ring_widget.dart
  1. import 'package:flutter/material.dart';

  2. class CircleRingWidget extends StatelessWidget {
  3.   late bool isShowRing;
  4.   late Color dotColor;
  5.   CircleRingWidget(this.isShowRing,this.dotColor, {super.key});
  6.   @override
  7.   Widget build(BuildContext context) {
  8.     return CustomPaint(
  9.       painter: CircleAndRingPainter(isShowRing,dotColor),
  10.       size: const Size(56.0, 81.0), // 调整尺寸大小
  11.     );
  12.   }
  13. }

  14. class CircleAndRingPainter extends CustomPainter {
  15.   late bool isShowRing;
  16.   late Color dotColor;
  17.   CircleAndRingPainter(this.isShowRing,this.dotColor);
  18.   @override
  19.   void paint(Canvas canvas, Size size) {
  20.     Paint circlePaint = Paint()
  21.       ..color = dotColor // 设置圆点的颜色
  22.       ..strokeCap = StrokeCap.round
  23.       ..strokeWidth = 1.0;

  24.     Paint ringPaint = Paint()
  25.       ..color = Colors.white // 设置圆环的颜色
  26.       ..strokeCap = StrokeCap.round
  27.       ..strokeWidth = 1.0
  28.       ..style = PaintingStyle.stroke;

  29.     Offset center = size.center(Offset.zero);

  30.     // 画一个半径为10的圆点
  31.     canvas.drawCircle(center, 13.0, circlePaint);

  32.     if(isShowRing){
  33.       // 画一个半径为20的圆环
  34.       canvas.drawCircle(center, 18.0, ringPaint);
  35.     }
  36.   }

  37.   @override
  38.   bool shouldRepaint(covariant CustomPainter oldDelegate) {
  39.     return false;
  40.   }
  41. }
复制代码
存储颜色和食指划过点的数据类

color_offset.dart
  1. import 'dart:ui';

  2. class ColorOffset{
  3.   late Color color;
  4.   late List<Offset> offsets=[];
  5.   ColorOffset(Color color,List<Offset> offsets){
  6.     this.color=color;
  7.     this.offsets.addAll(offsets);
  8.   }
  9. }
复制代码
具体涂鸦点的绘制
  1. 此处是具体绘制涂鸦点的自定义view。大家是不是觉得哇,好简单呢。两个循环一嵌套,瞬间所有涂鸦就都出来了。其实做这个功能时,我参考了其他各种涂鸦控件,但是总觉得流程非常复杂。难以理解。原因是他们的颜色和点在数据层面都是混合到一起的,而且还得判断哪里是新画的涂鸦线条,来回控制。用这个demo的结构,相信各位读者一看就能知道里面的思路
复制代码
doodle_painter.dart
  1. import 'package:flutter/cupertino.dart';

  2. import 'color_offset.dart';

  3. class DoodleImagePainter extends CustomPainter {
  4.   late Map<int,ColorOffset> newPoints;
  5.   DoodleImagePainter(this.newPoints);

  6.   @override
  7.   void paint(Canvas canvas, Size size) {
  8.     newPoints.forEach((key, value) {
  9.       Paint paint = _getPaint(value.color);
  10.       for(int i=0;i<value.offsets.length - 1;i++){
  11.         //最后一个画点,其他画线
  12.         if(i==value.offsets.length-1){
  13.           canvas.drawCircle(value.offsets[i], 2.0, paint);
  14.         }else{
  15.           canvas.drawLine(value.offsets[i], value.offsets[i + 1], paint);
  16.         }

  17.       }
  18.     });
  19.   }
  20.   Paint _getPaint(Color color){
  21.     return Paint()
  22.       ..color = color
  23.       ..strokeCap = StrokeCap.round
  24.       ..strokeWidth = 5.0;
  25.   }

  26.   @override
  27.   bool shouldRepaint(covariant CustomPainter oldDelegate) {
  28.     return true;
  29.   }
  30. }
复制代码
涂鸦主要界面代码


  • 包含整体涂鸦数据的构建
  • 包含涂鸦图片的合成和本地存储
  • 包含涂鸦颜色列表的自定义
  • 包含涂鸦原图片的放大缩小
  • 包含撤销一步和清屏功能
  1. 下面这些就是整体涂鸦相关功能代码,其中一些资源图片未提供,请根据需要自己去设计处获取。
复制代码
  1. import 'dart:io';
  2. import 'dart:typed_data';
  3. import 'dart:ui' as ui;

  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/rendering.dart';

  6. import '../player_base_control.dart';
  7. import 'circle_ring_widget.dart';
  8. import 'color_offset.dart';
  9. import 'doodle_painter.dart';

  10. class DoodleWidget extends PlayerBaseControllableWidget {
  11.   final String snapShotPath;
  12.   final ValueChanged<bool>? completed;
  13.   const DoodleWidget(super.controller,
  14.       {super.key, required this.snapShotPath, this.completed});

  15.   @override
  16.   State<StatefulWidget> createState() => _DoodleWidgetState();
  17. }

  18. class _DoodleWidgetState extends State<DoodleWidget> {
  19.   Map<int, ColorOffset> newPoints = {};
  20.   List<Offset> points = [];
  21.   int lineIndex = 0;
  22.   GlobalKey globalKey = GlobalKey();
  23.   int currentSelect = 0;
  24.   final double maxScale = 3.0;
  25.   final double minScale = 1.0;
  26.   List<Color> colors = const [
  27.     Color(0xffff0000),
  28.     Color(0xfffae03d),
  29.     Color(0xff6f52ff),
  30.     Color(0xffffffff),
  31.     Color(0xff000000)
  32.   ];
  33.   TransformationController controller = TransformationController();
  34.   double realScale = 1.0;
  35.   Offset realTransLocation = Offset.zero;
  36.   late Image currentImg;

  37.   bool isSaved = false;

  38.   @override
  39.   void initState() {
  40.     currentImg = Image.memory(File(widget.snapShotPath).readAsBytesSync());
  41.     controller.addListener(() {
  42.       ///获取矩阵里面的缩放具体值
  43.       realScale = controller.value.entry(0, 0);

  44.       ///获取矩阵里面的位置偏移量
  45.       realTransLocation = Offset(controller.value.getTranslation().x,
  46.           controller.value.getTranslation().y);
  47.     });
  48.     super.initState();
  49.   }

  50.   @override
  51.   Widget build(BuildContext context) {
  52.     return Stack(
  53.       children: [
  54.         Positioned.fill(child: LayoutBuilder(
  55.             builder: (BuildContext context, BoxConstraints constraint) {
  56.           return InteractiveViewer(
  57.             panEnabled: false,
  58.             scaleEnabled: true,
  59.             maxScale: maxScale,
  60.             minScale: minScale,
  61.             transformationController: controller,
  62.             onInteractionStart: (ScaleStartDetails details) {
  63.               // print("--------------onInteractionStart执行了  dx=${details.focalPoint.dx} dy=${details.focalPoint.dy}");
  64.             },
  65.             onInteractionUpdate: (ScaleUpdateDetails details) {
  66.               if (details.pointerCount == 1) {
  67.                 /// 获取 x,y 拿到值后进行缩放偏移等换算
  68.                 var x = details.focalPoint.dx;
  69.                 var y = details.focalPoint.dy;
  70.                 var point = Offset(
  71.                     _getScaleTranslateValue(x, realScale, realTransLocation.dx),
  72.                     _getScaleTranslateValue(
  73.                         y, realScale, realTransLocation.dy));
  74.                 setState(() {
  75.                   points.add(point);
  76.                   newPoints[lineIndex] =
  77.                       ColorOffset(colors[currentSelect], points);
  78.                 });
  79.               }
  80.             },
  81.             onInteractionEnd: (ScaleEndDetails details) {
  82.               // print("onInteractionEnd执行了");
  83.               if (points.length > 5) {
  84.                 newPoints[lineIndex] =
  85.                     ColorOffset(colors[currentSelect], points);
  86.                 lineIndex++;
  87.               }
  88.               // newPoints.addAll({lineIndex:ColorOffset(colors[currentSelect],points)});
  89.               //清空原数组
  90.               points.clear();
  91.             },
  92.             child: RepaintBoundary(
  93.               key: globalKey,
  94.               child: Stack(
  95.                 alignment: AlignmentDirectional.center,
  96.                 children: [
  97.                   Positioned.fill(child: currentImg),
  98.                   Positioned.fill(
  99.                       child:
  100.                           CustomPaint(painter: DoodleImagePainter(newPoints))),
  101.                 ],
  102.               ),
  103.             ),
  104.           );
  105.         })),
  106.         Positioned(
  107.           bottom: 0,
  108.           left: 0,
  109.           right: 0,
  110.           child: _bottomActions(),
  111.         )
  112.       ],
  113.     );
  114.   }

  115.   double _getScaleTranslateValue(
  116.       double current, double scale, double translate) {
  117.     return current / scale - translate / scale;
  118.   }

  119.   Widget _bottomActions() {
  120.     return Container(
  121.       height: 81,
  122.       color: const Color(0xaa17161f),
  123.       child: Row(
  124.         children: [
  125.           /// 关闭按钮
  126.           SizedBox(
  127.             width: 95,
  128.             height: 81,
  129.             child: Center(
  130.               child: GestureDetector(
  131.                 onTap: () {
  132.                   widget.completed?.call(false);
  133.                 },
  134.                 child: Image.asset(
  135.                   "images/icon_close_white.webp",
  136.                   width: 30,
  137.                   height: 30,
  138.                   scale: 3,
  139.                   package: "koo_daxue_record_player",
  140.                 ),
  141.               ),
  142.             ),
  143.           ),
  144.           const VerticalDivider(
  145.             thickness: 1,
  146.             indent: 15,
  147.             endIndent: 15,
  148.             color: Colors.white,
  149.           ),
  150.           Row(
  151.             children: _colorListWidget(),
  152.           ),
  153.           Expanded(child: Container()),

  154.           /// 退一步按钮
  155.           SizedBox(
  156.             width: 66,
  157.             height: 81,
  158.             child: GestureDetector(
  159.               onTap: () {
  160.                 setState(() {
  161.                   if (lineIndex > 0) {
  162.                     lineIndex--;
  163.                     newPoints.remove(lineIndex);
  164.                   }
  165.                 });
  166.               },
  167.               child: Center(
  168.                   child: Image.asset(
  169.                 lineIndex == 0
  170.                     ? "images/icon_undo.webp"
  171.                     : "images/icon_undo_white.webp",
  172.                 width: 30,
  173.                 height: 30,
  174.                 scale: 3,
  175.                 package: "koo_daxue_record_player",
  176.               )),
  177.             ),
  178.           ),

  179.           /// 清除按钮
  180.           SizedBox(
  181.             width: 66,
  182.             height: 81,
  183.             child: Center(
  184.                 child: GestureDetector(
  185.               onTap: () {
  186.                 setState(() {
  187.                   lineIndex = 0;
  188.                   newPoints.clear();
  189.                 });
  190.               },
  191.               child: Image.asset(
  192.                 lineIndex == 0
  193.                     ? "images/icon_clear_doodle.webp"
  194.                     : "images/icon_clear_doodle_white.webp",
  195.                 width: 30,
  196.                 height: 30,
  197.                 scale: 3,
  198.                 package: "koo_daxue_record_player",
  199.               ),
  200.             )),
  201.           ),
  202.           const VerticalDivider(
  203.             thickness: 1,
  204.             indent: 15,
  205.             endIndent: 15,
  206.             color: Colors.white,
  207.           ),

  208.           /// 确定按钮
  209.           SizedBox(
  210.             width: 85,
  211.             height: 81,
  212.             child: Center(
  213.                 child: GestureDetector(
  214.               onTap: () {
  215.                 if (isSaved) return;
  216.                 isSaved = true;
  217.                 if (newPoints.isEmpty) {
  218.                   widget.completed?.call(false);
  219.                   return;
  220.                 }
  221.                 saveDoodle(widget.snapShotPath).then((value) {
  222.                   if (value) {
  223.                     widget.completed?.call(true);
  224.                   } else {
  225.                     widget.completed?.call(false);
  226.                   }
  227.                 });
  228.               },
  229.               child: Image.asset(
  230.                 "images/icon_finish_white.webp",
  231.                 width: 30,
  232.                 height: 30,
  233.                 scale: 3,
  234.                 package: "koo_daxue_record_player",
  235.               ),
  236.             )),
  237.           )
  238.         ],
  239.       ),
  240.     );
  241.   }

  242.   List<Widget> _colorListWidget() {
  243.     List<Widget> widgetList = [];
  244.     for (int i = 0; i < colors.length; i++) {
  245.       Color color = colors[i];
  246.       widgetList.add(GestureDetector(
  247.         onTap: () {
  248.           setState(() {
  249.             currentSelect = i;
  250.           });
  251.         },
  252.         child: CircleRingWidget(i == currentSelect, color),
  253.       ));
  254.     }
  255.     return widgetList;
  256.   }

  257.   Future<bool> saveDoodle(String imgPath) async {
  258.     try {
  259.       RenderRepaintBoundary boundary =
  260.           globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
  261.       ui.Image image = await boundary.toImage(pixelRatio: 3.0);
  262.       ByteData? byteData =
  263.           await image.toByteData(format: ui.ImageByteFormat.png);
  264.       Uint8List pngBytes = byteData!.buffer.asUint8List();
  265.       // 保存图片到文件
  266.       File imgFile = File(imgPath);
  267.       await imgFile.writeAsBytes(pngBytes);
  268.       return true;
  269.     } catch (e) {
  270.       return false;
  271.     }
  272.   }
  273. }
复制代码
以上就是Flutter实现给图片添加涂鸦功能的详细内容,更多关于Flutter给图片添加涂鸦的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
21
积分
22
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2024-12-28 15:13:13 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
20
积分
20
注册时间
2022-12-27
最后登录
2022-12-27

发表于 2025-2-25 12:40:55 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表