Flutter笔记:手写一个简单的画板工具

news2024/11/25 11:50:53
Flutter笔记
手写一个简单的画板工具

作者李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
邮箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/133418742



1. 任务介绍

在本文中,我们将一起开发一个基本的Flutter画板应用,用户可以在画板上自由绘制,选择不同的颜色来绘制线条。这个画板应用将允许用户通过点击颜色选择按钮来选择画笔的颜色,并提供鼠标光标支持以增强用户体验。

任务要求

  1. 创建一个Flutter应用,包含一个画板界面,初始时,画板上没有任何绘制内容。
  2. 实现颜色选择功能,用户可以点击应用栏中的颜色选择按钮,弹出颜色选择对话框,选择绘制颜色。
  3. 支持自由绘制功能,用户可以使用鼠标或触摸屏在画板上自由绘制线条。绘制时,使用所选的颜色。
  4. 当用户在画板上绘制时,应实时显示他们的绘制内容。
  5. 用户可以使用鼠标光标,绘制不同颜色的线条。

效果预览

在这里插入图片描述

预期收获

任务完成后,你将具备以下技能:

  • Flutter应用开发基础知识。
  • 实现用户界面中的颜色选择和绘制功能。
  • 处理用户输入和手势操作。
  • 自定义绘制,使用自定义绘制器和Canvas API。

这个任务将有助于您深入了解Flutter应用的开发,以及如何实现一个具有基本绘图功能的用户界面。

2. 知识点准备

本文将使用以下知识点,最终实现

  1. MouseRegion: MouseRegion 用于捕获鼠标事件,允许您指定鼠标在其内部时的行为,包括设置鼠标光标的外观(在这里使用了 SystemMouseCursors.click)。

  2. SystemMouseCursors: SystemMouseCursors 是一个用于指定不同系统鼠标光标样式的类。在这里,我们使用了 SystemMouseCursors.click,将鼠标光标设置为点击样式,以提供用户视觉反馈。

  3. GestureDetector: GestureDetector 用于捕获用户手势事件,如拖动手势(onPanUpdateonPanEnd)。它允许您检测用户的绘制动作并触发相应的回调。

  4. CustomPaint: CustomPaint 是一个用于自定义绘制的容器,它接受一个自定义的 CustomPainter 对象。在这里,它用于呈现用户的绘制。

  5. CustomPainter: CustomPainter 是一个抽象类,它包含了用于自定义绘制的方法。在这里,我们创建了 MyPainter 类来实现绘制用户绘制的笔画。

  6. RenderBox: RenderBox 是一个用于获取渲染对象的边界框和坐标的类。在这里,我们使用它来获取鼠标事件的本地坐标,并将其转换为相对于 CustomPaint 的坐标。

  7. Canvas: Canvas 是一个用于绘制 2D 图形的画布。我们使用它在 MyPainter 中绘制用户的笔画。

  8. PointModePointMode 类用于指定如何绘制点的枚举类。在画板应用中,它有两种可能的值:

    • PointMode.points: 这个模式用于绘制单个点。当用户在画板上点击并松开鼠标时,将使用此模式来绘制点,以实现单击绘制的效果。
    • PointMode.polygon: 这个模式用于绘制连接的线条。当用户在画板上拖动鼠标时,将使用此模式来绘制连接的线条,以实现绘制笔画的效果。

    PointMode 通过 canvas.drawPoints 方法在 CustomPainter 中使用,用于指定如何绘制用户的绘制。对于单击,我们使用 PointMode.points 来绘制点,而对于笔画,我们使用 PointMode.polygon 来绘制连接的线条。
    该类包含在dart:ui库中。

  9. flutter_colorpicker 模块:这是一个选择颜色的第三方模块,你可以参考其官方文档

3. 代码实现与效果

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: DrawingBoard(),
    );
  }
}

/// 画板应用的主页 [StatefulWidget]。
class DrawingBoard extends StatefulWidget {
  const DrawingBoard({super.key});

  
  State<DrawingBoard> createState() => _DrawingBoardState();
}

/// [DrawingBoard] 的状态类,负责管理用户绘制的功能和界面。
class _DrawingBoardState extends State<DrawingBoard> {
  List<DrawObject> drawObjects = [];
  Color selectedColor = Colors.black;
  bool isFirstDraw = true;

  /// 显示颜色选择对话框的方法。
  void selectColor() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        Color newColor = selectedColor;
        return AlertDialog(
          title: const Text('选择颜色'),
          content: SingleChildScrollView(
            child: ColorPicker(
              pickerColor: selectedColor,
              onColorChanged: (color) {
                newColor = color;
              },
            ),
          ),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                setState(() {
                  selectedColor = newColor;
                });
                Navigator.of(context).pop();
              },
              child: const Text('确定'),
            ),
          ],
        );
      },
    );
  }

  
  Widget build(BuildContext context) {
    // 创建一个鼠标光标,使用Icons.draw_outlined图标
    const mouseCursor = SystemMouseCursors.click;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Jack Lee 的画板'),
        actions: [
          IconButton(
            icon: const Icon(Icons.color_lens),
            onPressed: () {
              selectColor();
            },
          ),
        ],
      ),
      body: MouseRegion(
        cursor: mouseCursor, // 使用自定义光标
        child: GestureDetector(
          onPanUpdate: (details) {
            setState(() {
              RenderBox renderBox = context.findRenderObject() as RenderBox;
              final offset = renderBox.globalToLocal(details.localPosition);
              if (isFirstDraw) {
                drawObjects.add(DrawObject([], selectedColor));
                isFirstDraw = false;
              }
              if (drawObjects.isNotEmpty) {
                drawObjects.last.points.add(offset);
              }
            });
          },
          onPanEnd: (details) {
            setState(() {
              drawObjects.add(DrawObject([], selectedColor));
              isFirstDraw = true;
            });
          },
          child: CustomPaint(
            painter: MyPainter(drawObjects),
            size: Size.infinite,
          ),
        ),
      ),
    );
  }
}

/// 自定义画板的绘制器 [CustomPainter]。
class MyPainter extends CustomPainter {
  final List<DrawObject> drawObjects;

  MyPainter(this.drawObjects);

  
  void paint(Canvas canvas, Size size) {
    for (final drawObject in drawObjects) {
      final paint = Paint()
        ..color = drawObject.color
        ..strokeCap = StrokeCap.round
        ..strokeWidth = 5.0;

      if (drawObject.points.length > 1) {
        for (int i = 0; i < drawObject.points.length - 1; i++) {
          if (drawObject.points[i] != null &&
              drawObject.points[i + 1] != null) {
            canvas.drawLine(
                drawObject.points[i]!, drawObject.points[i + 1]!, paint);
          }
        }
      } else if (drawObject.points.isNotEmpty) {
        canvas.drawPoints(
            PointMode.points, drawObject.points.cast<Offset>(), paint);
      }
    }
  }

  
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

/// 用户绘制的对象类,包括点和颜色。
class DrawObject {
  final List<Offset?> points;
  final Color color;

  DrawObject(this.points, this.color);
}

效果如图所示:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1050652.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法基础之归并排序

一、归并排序的形象理解 原题链接 示例代码 void merge_sort(int q[], int l, int r) {if (l > r) return;int mid l r >> 1;merge_sort(q, l, mid), merge_sort(q, mid 1, r);int k 0, i l, j mid 1;while (i < mid && j < r) //第一处if (q[i]…

基于Spider的全站数据爬取

踩坑 一开始运行的时候会出来很多其他的日志信息&#xff0c;这里我忘了设置settings.py中LOG_LEVELERROR 获取xpath 这里获取xpath比较简单。 首先发现所有的照片文字都是在li标签下的&#xff0c;所以第一步是获取所有的li标签&#xff0c;得到li标签的列表。 li_list re…

【sgTileImage】自定义组件:瓦片图拖拽局部加载、实现以鼠标为中心缩放

特性&#xff1a; 支持缩放瓦片图&#xff0c;定义瓦片图初始缩放比例&#xff0c;以鼠标所在位置为中心缩放支持局部拖拽加载 sgTileImage源码 <template><div :class"$options.name"><div class"sg-ctrl"><label>缩放百分比&l…

电脑怎么用U盘重装系统-电脑用U盘重装Win10系统的步骤

电脑怎么用U盘重装系统&#xff1f;电脑对于当前日常办公生活是特别重要的&#xff0c;但是&#xff0c;随着操作时间的增加&#xff0c;电脑内的操作系统运作可能会变得越来越缓慢了。这时候重装系统成为解决系统问题的有效方法。下面小编给大家介绍利用U盘给电脑重装系统Win1…

Python入门教程48:Pycharm永久镜像源的pip配置方法

国内几个好用的Python镜像服务器地址&#xff1a; 清华大学镜像站&#xff1a;https://pypi.tuna.tsinghua.edu.cn/simple/阿里云镜像站&#xff1a;https://mirrors.aliyun.com/pypi/simple/中科大镜像站&#xff1a;https://pypi.mirrors.ustc.edu.cn/simple/中国科技大学镜…

HTTP 错误 401.3 - Unauthorized 由于 Web 服务器上此资源的访问控制列表(ACL)配置或加密设置,您无权查看此目录或页面。

用IIS 发布网站&#xff0c;不能访问且出现错误&#xff1a;HTTP 错误 401.3 - Unauthorized 由于Web服务器上此资源的访问控制列表(ACL)配置或加密设置。您无权查看此目录或页面 问题截图&#xff1a; 问题描述&#xff1a;HTTP 错误 401.3 - 未经授权&#xff1a;访问由于 A…

anzo capital昂首资本详解MT4和MT5订单执行方式

很多投资者在后台咨询anzo capital昂首资本&#xff0c;MT4和MT5订单执行方式有什么不同&#xff0c;今天一起探讨! MT4平台提供了三种类型的订单执行方式&#xff1a; 第一种是即时执行。当交易者向经纪人发送建立订单的请求时&#xff0c;平台将自动以当前价格录入该订单。…

《YOLOv5:从入门到实战》报错解决 专栏答疑

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。《YOLOv5&#xff1a;从入门到实战》专栏上线后&#xff0c;部分同学在学习过程中提出了一些问题&#xff0c;笔者相信这些问题其他同学也有可能遇到。为了让大家可以更好地学习本专栏内容&#xff0c;笔者特意推出了该篇专…

Java 实现遍历一个文件夹,文件夹有100万数据,获取到修改时间在2天之内的数据

目录 1 需求2 实现1&#xff08;第一种方法&#xff09;2 实现2 &#xff08;推荐使用这个&#xff0c;快&#xff09;3 实现3&#xff08;推荐&#xff09; 1 需求 现在有一个文件夹&#xff0c;里面会一直存数据&#xff0c;动态的存数据&#xff0c;之后可能会达到100万&am…

【接口测试】HTTP协议

一、HTTP 协议基础 HTTP 简介 HTTP 是一个客户端终端&#xff08;用户&#xff09;和服务器端&#xff08;网站&#xff09;请求和应答的标准&#xff08;TCP&#xff09;。通常是由客户端发起一个请求&#xff0c;创建一个到服务器的 TCP 连接&#xff0c;当服务器监听到客户…

【Java 进阶篇】MySQL数据库范式详解

范式是数据库设计中的一种理论方法&#xff0c;旨在通过减少数据冗余来提高数据存储的有效性和完整性。在MySQL数据库中&#xff0c;范式设计是一个重要的概念&#xff0c;它有助于组织和管理数据&#xff0c;确保数据的一致性和可靠性。本文将深入探讨数据库范式&#xff0c;包…

必备基础算法

目录 一、双指针 双指针与链表双指针与链表 二、前缀和 一维二维 三、差分 一维二维 四、深度搜索&#xff0c;dfs dfs数组排列dfs岛屿问题dfs染色法&#xff08;二分图判定&#xff09;dfs路径规划dfs拓扑排序 五、广度搜索&#xff0c;bfs bfs最优路径规划bfsdijkstra 六、单…

Python与数据分析--Pandas操作进阶

目录 1.文件读取方式 1.1.绝对路径读取文件 1.2.相对路径读取文件 2.列表数据操作 2.1.列索引指定 2.2.代码数据对齐 3.创建新CSV文件 4.缺失值处理 4.1.缺失值创建 4.2.缺失值检索 4.3.缺失值查询 4.3.1.isnull()函数判断 4.3.2.notnull()函数判断 4.3.3.any()函数…

数据集笔记:2015上海地铁一卡通数据

数据地址&#xff1a;上海地铁数据_免费高速下载|百度网盘-分享无限制 (baidu.com) 数据介绍 上海2015年几天的地铁一卡通出入站信息 卡号、交易日期、交易时间、公交线路/地铁站点中文名称、行业名称(公交、地铁、出租、轮渡、PR停车场)、交易金额、交易性质(非优惠、优惠、…

3种Renko图表形态FPmarkets3秒轻松判断价格走势

Renko图表形态在交易中的应用并不逊色于其他技术分析方法。相较于普通的烛台图表&#xff0c;使用Renko图表时&#xff0c;有些经典模式更容易被发现和识别&#xff0c;FPmarkets总结这些模式包括&#xff1a; 首先是头和肩膀形态。这是一种价格反转形态&#xff0c;由两个较小…

【软件测试】软件缺陷报告如何编写

废话不多说&#xff0c;三张图说明 软件缺陷报告如何编写 以及 报告的跟踪流程 软件缺陷报告格式 软件缺陷报告内容说明 缺陷状态 - 分为 新建、打开、修复、关闭 - 新建 - 测试人员第一次发现缺陷 - 打开 - 测试将报告交给开发&#xff0c;开发确认缺陷&#xff0c;准备动手…

threejs中模型自定义路线移动

threejs中模型自定义路线移动 生命不息&#xff0c;学习不止 基于r95Threejs版本 此例子中&#xff1a;包括背景设置&#xff1a;天空之盒。 模型的引用&#xff1a;小车和整体 glb模型引用 路线设置(因线line2无法设置宽度,所以选择了用管道&#xff0c;当然也可用点成面&…

MySQL 索引介绍和最佳实践

目录 一、前言二、索引类型1.1 主键索引&#xff08;PRIMARY KEY&#xff09;1.2 唯一索引&#xff08;UNIQUE&#xff09;1.3 普通索引&#xff08;NORMAL&#xff09;1.3.1 单列普通索引1.3.2 单列前缀普通索引1.3.3 多列普通索引1.3.4 多列前缀普通索引 1.4 空间索引&#x…

商场做小程序商城的作用是什么?

商场是众多商家聚集在一起的购物公共场所&#xff0c;大商场也往往入驻着众多行业商家&#xff0c;是每个城市重要的组成部分。 随着互联网电商深入及客户消费行为改变&#xff0c;不少商场如今的客流量非常有限&#xff0c;甚至可以说是员工比客人多&#xff0c;这就导致撤店…

三相Vienna整流器电流畸变的抑制方法

该博客参考丁文龙的博士论文《低成本充电系统高性能多端口Vienna整流器关键控制策略研究》&#xff0c;他的博士论文深入浅出&#xff0c;分析透彻。感谢师妹Miss Young提供的技术指导&#xff0c;她是一位优秀的电力电子工程师&#xff0c;祝她事业顺利&#xff0c;身体健康。…