当前位置: 首页 > news >正文

Flutter 实战:simple_paint 手绘画板的手势采样、CustomPainter 绘制与鸿蒙适配解析

Flutter 实战:simple_paint 手绘画板的手势采样、CustomPainter 绘制与鸿蒙适配解析

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

手绘画板是 Flutter 里非常适合练习触控、Canvas 和状态管理的项目。它不像普通表单页面那样只处理点击和输入,而是要持续采样用户手指移动轨迹,把点位组织成笔画,再交给CustomPainter绘制到画布上。

simple_paint是一个轻量画板应用,功能包括:颜色选择、笔刷粗细调节、橡皮模式、撤销最后一笔、清空画布、实时绘制当前笔画。它的实现没有复杂三方库,核心都在 Flutter 的手势系统、CustomPaint和 Dart 数据结构中,非常适合写成一篇完整的源码解析文章。

画板类应用的核心不是“画一条线”,而是如何把连续触控点、笔画状态、绘制参数和重绘时机组织清楚。

图示说明:上图展示 Flutter 页面在移动端的布局组织方式。simple_paint的实际界面由颜色调色盘、笔粗滑块、橡皮按钮、画布和撤销清空按钮组成。

一、项目定位与功能边界

1.1 应用定位

simple_paint是一个轻量手绘画板应用,用于演示 Flutter 中手势采样、线段绘制、画笔参数控制和画布状态管理。它适合学习GestureDetectorCustomPainterCanvas.drawLine和状态驱动画面更新。

项目当前支持:

  • 横向颜色调色盘。
  • 选择 10 种内置颜色。
  • 通过滑块调节笔刷粗细。
  • 使用圆点预览当前笔刷大小。
  • 橡皮模式。
  • 按笔画粒度撤销。
  • 清空画布。
  • 显示当前已完成笔画数量。
  • 实时绘制当前正在拖动的笔画。

1.2 功能模块

功能模块页面表现源码实现
调色盘横向颜色圆点_colors+ListView.builder
当前颜色选中圆点蓝色边框_selectedColor
笔刷粗细Slider 与预览圆点_strokeWidth
橡皮模式Eraser/Erasing 按钮_isErasing
手势采样拖动画布生成线条_onPanStart_onPanUpdate_onPanEnd
笔画存储已完成线条列表_lines
绘制引擎Canvas 上连接点位_Painter.paint
撤销清空AppBar 图标按钮_undo()_clearCanvas()

1.3 技术栈

技术点使用位置价值
Flutter页面、按钮、滑块、手势、画布构建跨端画板 UI
Dart列表、模型类、状态逻辑管理笔画数据
Material 3应用主题与控件样式useMaterial3: true
GestureDetector拖拽事件采样获取绘制点位
CustomPainter自定义绘制在 Canvas 上画线

二、工程结构与运行环境

2.1 工程结构

simple_paint是标准 Flutter 工程,主逻辑位于lib/main.dart

文件或目录作用
lib/main.dart应用入口、画板状态、手势处理、绘制逻辑
pubspec.yamlFlutter SDK 与测试依赖声明
test/widget_test.dartWidget 测试入口
ohos/鸿蒙平台工程目录
analysis_options.yamlDart 静态分析规则

2.2 运行命令

flutter doctor flutter pub get flutter run

项目没有引入复杂三方绘图库,绘制能力完全来自 Flutter 自带的CustomPaint和 Canvas API。

2.3 依赖声明

dependencies:flutter:sdk:fluttercupertino_icons:^1.0.8dev_dependencies:flutter_test:sdk:flutterflutter_lints:^5.0.0

这种依赖结构适合做鸿蒙侧基础绘制验证:业务逻辑在 Dart 层,重点观察触控事件、画线效果、Canvas 性能和高 DPI 下的线条质量。

三、应用入口与主题配置

3.1 main 函数

Flutter 应用从main()启动:

import'package:flutter/material.dart';voidmain(){runApp(constSimplePaintApp());}

入口函数只负责加载根组件,不处理绘图状态。

3.2 根组件

classSimplePaintAppextendsStatelessWidget{constSimplePaintApp({super.key});@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'Simple Paint',theme:ThemeData(colorScheme:ColorScheme.fromSeed(seedColor:Colors.blue),useMaterial3:true,),home:constSimplePaintHomePage(title:'Simple Paint'),);}}

根组件负责应用标题和主题配置。绘图数据、笔刷设置和交互逻辑由首页 State 维护。

3.3 主题色

colorScheme:ColorScheme.fromSeed(seedColor:Colors.blue)

蓝色主题用于 AppBar 和选中态边框,与画板工具类应用的冷静风格比较匹配。

四、StatefulWidget 与画板状态

4.1 首页组件

classSimplePaintHomePageextendsStatefulWidget{constSimplePaintHomePage({super.key,requiredthis.title});finalStringtitle;@overrideState<SimplePaintHomePage>createState()=>_SimplePaintHomePageState();}

画板需要持续响应用户拖拽、颜色选择、笔粗变化、橡皮切换、撤销和清空,因此使用StatefulWidget

4.2 核心状态字段

List<DrawnLine>_lines=[];List<Offset>_currentLine=[];Color_selectedColor=Colors.black;double _strokeWidth=3.0;bool _isErasing=false;
状态字段类型作用
_linesList<DrawnLine>已完成笔画
_currentLineList<Offset>当前正在绘制的点位
_selectedColorColor当前画笔颜色
_strokeWidthdouble当前画笔粗细
_isErasingbool是否处于橡皮模式

4.3 调色盘列表

finalList<Color>_colors=[Colors.black,Colors.red,Colors.orange,Colors.yellow,Colors.green,Colors.blue,Colors.purple,Colors.pink,Colors.brown,Colors.grey,];

调色盘内置 10 种颜色,覆盖常见手绘场景。

五、笔画模型 DrawnLine

5.1 模型类定义

classDrawnLine{List<Offset>points;Colorcolor;double strokeWidth;DrawnLine({requiredthis.points,requiredthis.color,requiredthis.strokeWidth,});}

每一笔由点位、颜色和粗细组成。

5.2 为什么按笔画存储

按笔画存储有几个好处:

  1. 撤销时可以删除最后一笔。
  2. 每一笔可以保留自己的颜色。
  3. 每一笔可以保留自己的粗细。
  4. 绘制时可以按笔画遍历。

5.3 数据结构关系

_lines -> DrawnLine -> points: List<Offset> -> color: Color -> strokeWidth: double

这种结构比单纯保存所有点更适合画板应用,因为它保留了每一笔的上下文。

六、手势采样流程

6.1 开始绘制

void_onPanStart(DragStartDetailsdetails){setState((){_currentLine=[details.localPosition];});}

手指按下并开始拖动时,使用当前位置创建新的当前笔画。

6.2 持续追加点位

void_onPanUpdate(DragUpdateDetailsdetails){setState((){_currentLine.add(details.localPosition);});}

拖动过程中,每次更新都会把新的本地坐标加入_currentLine

6.3 结束绘制

void_onPanEnd(DragEndDetailsdetails){setState((){_lines.add(DrawnLine(points:List.from(_currentLine),color:_isErasing?Colors.white:_selectedColor,strokeWidth:_isErasing?_strokeWidth*3:_strokeWidth,));_currentLine=[];});}

拖动结束后,把当前点位复制到新的DrawnLine中,并清空_currentLine

七、画笔与橡皮逻辑

7.1 正常画笔

正常模式下,笔画使用当前选中颜色和当前笔刷粗细。

color:_selectedColor,strokeWidth:_strokeWidth,

7.2 橡皮模式

橡皮本质上是用白色粗线覆盖已有线条。

color:_isErasing?Colors.white:_selectedColor,strokeWidth:_isErasing?_strokeWidth*3:_strokeWidth,

这种实现简单直接,适合白色背景画布。

7.3 真实限制

因为橡皮是白色绘制,不是真正删除历史线段,所以如果未来画布背景改成透明、图片或其他颜色,橡皮逻辑也要同步调整。

橡皮有两种思路:一种是白色覆盖,另一种是修改历史路径。当前项目采用的是白色覆盖,简单但依赖白色画布背景。

八、调色盘实现

8.1 横向颜色列表

SizedBox(height:60,child:ListView.builder(scrollDirection:Axis.horizontal,padding:constEdgeInsets.symmetric(horizontal:16,vertical:8),itemCount:_colors.length,itemBuilder:(context,index){finalcolor=_colors[index];finalisSelected=_selectedColor==color;returnGestureDetector(...);},),)

横向列表适合展示调色盘,不占用太多竖向空间。

8.2 选择颜色

onTap:(){setState((){_selectedColor=color;_isErasing=false;});}

选择颜色时会自动关闭橡皮模式,避免用户以为切换颜色后仍在绘制,实际却还在擦除。

8.3 选中态

border:Border.all(color:isSelected?Colors.blue:Colors.grey.shade300,width:isSelected?3:1,)

选中颜色使用更粗的蓝色边框,并显示勾选图标。

九、笔刷粗细控制

9.1 Slider 实现

Slider(value:_strokeWidth,min:1,max:20,onChanged:(value){setState((){_strokeWidth=value;});},)

笔刷粗细范围是 1 到 20,适合从细线到粗线的基础绘制。

9.2 粗细预览

Container(width:_strokeWidth*2,height:_strokeWidth*2,decoration:BoxDecoration(color:_selectedColor,shape:BoxShape.circle,),)

右侧圆点会随笔刷粗细变大或变小,让用户在绘制前看到当前笔刷大致效果。

9.3 交互意义

控制项影响
Slider改变_strokeWidth
预览圆点展示当前粗细
橡皮模式实际粗细放大 3 倍

十、橡皮按钮与笔画计数

10.1 橡皮按钮

ElevatedButton.icon(onPressed:(){setState((){_isErasing=!_isErasing;});},icon:Icon(_isErasing?Icons.edit:Icons.auto_fix_high),label:Text(_isErasing?'Erasing':'Eraser'),style:ElevatedButton.styleFrom(backgroundColor:_isErasing?Colors.orange:Colors.grey,),)

按钮文案、图标和颜色都会根据橡皮状态变化。

10.2 笔画计数

Text('${_lines.length}strokes')

这里统计的是已经完成的笔画数量,不包含当前正在拖动但尚未结束的一笔。

10.3 状态表

状态图标文案绘制颜色
画笔auto_fix_highEraser_selectedColor
橡皮editErasingWhite

十一、撤销与清空

11.1 撤销最后一笔

void_undo(){if(_lines.isNotEmpty){setState((){_lines.removeLast();});}}

撤销是按笔画粒度执行的,不是按点位粒度。

11.2 清空画布

void_clearCanvas(){setState((){_lines=[];_currentLine=[];});}

清空会同时删除历史笔画和当前正在绘制的点位。

11.3 AppBar 操作

actions:[IconButton(onPressed:_undo,icon:constIcon(Icons.undo),tooltip:'Undo',),IconButton(onPressed:_clearCanvas,icon:constIcon(Icons.delete),tooltip:'Clear',),]

撤销和清空放在 AppBar,符合工具类应用的常见操作习惯。

十二、CustomPainter 绘制流程

12.1 CustomPaint 接入

CustomPaint(painter:_Painter(lines:_lines,currentLine:_currentLine,currentColor:_isErasing?Colors.white:_selectedColor,currentStrokeWidth:_isErasing?_strokeWidth*3:_strokeWidth,),size:Size.infinite,)

CustomPaint接收历史笔画、当前笔画和当前绘制参数。

12.2 绘制历史笔画

for(finallineinlines){finalpaint=Paint()..color=line.color..strokeWidth=line.strokeWidth..strokeCap=StrokeCap.round..style=PaintingStyle.stroke;for(int i=0;i<line.points.length-1;i++){canvas.drawLine(line.points[i],line.points[i+1],paint);}}

每一笔由多个点组成,相邻点之间用drawLine连接。

12.3 绘制当前笔画

if(currentLine.isNotEmpty){finalpaint=Paint()..color=currentColor..strokeWidth=currentStrokeWidth..strokeCap=StrokeCap.round..style=PaintingStyle.stroke;for(int i=0;i<currentLine.length-1;i++){canvas.drawLine(currentLine[i],currentLine[i+1],paint);}}

当前笔画单独绘制,用户拖动时可以实时看到线条。

十三、画布布局与手势区域

13.1 画布区域

Expanded(child:GestureDetector(onPanStart:_onPanStart,onPanUpdate:_onPanUpdate,onPanEnd:_onPanEnd,child:Container(decoration:BoxDecoration(color:Colors.white,border:Border.all(color:Colors.grey.shade300),),child:CustomPaint(...),),),)

画布使用Expanded占据剩余空间,工具栏位于上方。

13.2 localPosition

手势回调使用details.localPosition,表示相对于当前 GestureDetector 区域的坐标。

details.localPosition

这非常适合画布绘制,因为 Canvas 坐标系也基于当前绘制区域。

13.3 shouldRepaint

@overrideboolshouldRepaint(covariant_Painter oldDelegate){returntrue;}

当前实现每次状态变化都允许重绘,简单可靠。对于轻量画板足够,但笔画数量很大时可以进一步优化。

十四、边界场景与真实限制

14.1 单点点击

如果只点一下不拖动,当前线条可能只有一个点。绘制逻辑通过points.length - 1控制循环,不会绘制出线段。

14.2 撤销粒度

撤销删除的是最后一条DrawnLine,也就是最后一笔。它不会撤销某一笔中的一小段。

14.3 橡皮限制

橡皮是白色覆盖,不是真正擦除历史数据。如果后续要导出透明图片或使用非白色背景,需要重新设计橡皮逻辑。

14.4 性能限制

shouldRepaint始终返回 true,笔画数量多时会反复绘制所有历史线条。简单项目可以接受,大型画板需要分层缓存或图片快照。

十五、Widget 测试设计

15.1 基础渲染测试

import'package:flutter_test/flutter_test.dart';import'../lib/main.dart';voidmain(){testWidgets('simple paint renders toolbar',(tester)async{awaittester.pumpWidget(constSimplePaintApp());expect(find.text('Simple Paint'),findsWidgets);expect(find.text('Stroke:'),findsOneWidget);expect(find.text('Eraser'),findsOneWidget);});}

这个测试验证根组件和工具栏元素。

15.2 橡皮状态测试

testWidgets('eraser button toggles erasing state',(tester)async{awaittester.pumpWidget(constSimplePaintApp());awaittester.tap(find.text('Eraser'));awaittester.pump();expect(find.text('Erasing'),findsOneWidget);});

这个测试覆盖_isErasing和按钮文案变化。

15.3 绘制手势测试

testWidgets('drawing gesture increases stroke count',(tester)async{awaittester.pumpWidget(constSimplePaintApp());finalgesture=awaittester.startGesture(constOffset(100,300));awaitgesture.moveTo(constOffset(150,350));awaitgesture.up();awaittester.pump();expect(find.text('1 strokes'),findsOneWidget);});

这个测试模拟拖动,验证笔画数量是否增加。

15.4 测试命令

fluttertest

保持测试中的根组件名称与实际源码一致,可以避免默认模板测试残留造成编译失败。

十六、鸿蒙适配观察

16.1 适配优势

simple_paint没有复杂原生插件,核心能力由 Flutter 手势和 Canvas 完成,适合验证鸿蒙侧基础绘制能力。

维度当前项目情况鸿蒙侧关注点
触控输入GestureDetector拖动采样密度和延迟
绘制能力CustomPainter线条质量和重绘性能
工具栏ListViewSlider、按钮控件可点击性
橡皮模式白色粗线覆盖背景一致性
撤销清空列表状态变化重绘后画布同步

16.2 构建命令参考

flutter clean flutter pub get flutter build hap

具体命令取决于所使用的鸿蒙 Flutter 适配环境。这个项目主要验证拖拽、画线、笔刷、橡皮和撤销清空。

16.3 运行验证要点

  1. 应用能正常启动到画板页面。
  2. 手指拖动画布可以连续绘线。
  3. 调色盘选色后新笔画颜色正确。
  4. Slider 调整后笔刷粗细变化。
  5. 橡皮模式能以白色粗线覆盖内容。
  6. 撤销和清空后画布状态正确刷新。

鸿蒙适配中,画板类应用要重点观察触控事件连续性、Canvas 重绘性能、线条圆角效果和高刷新拖动时的延迟。

十七、性能与可维护性

17.1 性能特征

项目当前适合轻量绘制,笔画数量少时性能压力不大。

维度当前表现
绘制方式每次重绘遍历历史笔画
当前笔画实时绘制
撤销粒度一整笔
橡皮方式白色覆盖
重绘策略shouldRepaint始终 true

17.2 当前结构优点

  • 手势采样方法职责清楚。
  • 笔画模型保留颜色和粗细。
  • 当前笔画和历史笔画分开管理。
  • 撤销逻辑简单稳定。
  • 工具栏、画布和绘制类边界清晰。

17.3 可演进方向

可以把DrawnLine改成不可变模型:

classDrawnLine{constDrawnLine({requiredthis.points,requiredthis.color,requiredthis.strokeWidth,});finalList<Offset>points;finalColorcolor;finaldouble strokeWidth;}

不可变模型更适合复杂画板状态管理,也能减少意外修改历史笔画。

十八、常见问题与优化建议

18.1 为什么用CustomPainter

因为画板需要直接在 Canvas 上绘制线段。普通 Widget 更适合布局和控件,CustomPainter更适合点、线、路径这类自定义图形。

18.2 为什么用相邻点连线

拖动过程中采样到的是一串离散点。把相邻点用drawLine连接,就能形成连续笔画。

18.3 为什么橡皮使用白色

当前画布背景是白色,所以白色粗线可以达到擦除视觉效果。这个实现简单,但依赖白色背景。

18.4 为什么撤销只删最后一笔

笔画按DrawnLine保存,因此撤销最自然的粒度是一笔。要实现更细粒度撤销,需要改变数据结构。

18.5 为什么shouldRepaint返回 true

画布状态频繁变化,始终重绘能保证显示正确。轻量项目可以这样写,复杂项目再做重绘优化。

18.6 为什么适合做鸿蒙适配示例

它覆盖了触控采样、Canvas 绘制、滑块、横向调色盘、按钮和状态刷新,能很好验证 Flutter 画板类应用在鸿蒙侧的基础表现。

总结

simple_paint用一个 Flutter 页面实现了轻量手绘画板的完整闭环:GestureDetector采集拖拽点位,DrawnLine保存每一笔的点、颜色和粗细,CustomPainter在 Canvas 上按相邻点连线绘制,工具栏负责颜色、粗细、橡皮、撤销和清空。

从工程角度看,这个项目的结构很适合学习 Flutter 自定义绘制。它把当前笔画和历史笔画分开管理,把绘制逻辑集中到_Painter,让状态和渲染边界比较清楚。

从鸿蒙适配角度看,重点是验证拖拽采样连续性、Canvas 绘制质量、笔刷粗细、橡皮覆盖、撤销清空和不同屏幕尺寸下的工具栏表现。处理好这些细节后,画板体验会更稳定。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

  • Flutter 官方文档
  • Flutter 测试文档
  • OpenHarmony 官网
http://www.gsyq.cn/news/1513474.html

相关文章:

  • OpenCore Configurator:黑苹果引导配置的终极可视化工具指南
  • 从‘烤机’到‘炼丹’:聊聊不同场景下CUDA线程配置的实战经验(附V100/A100对比)
  • NXP i.MX 6 SABRE开发板:从硬件参考设计到产品实战全解析
  • 面向对象:this关键字;构造器
  • 2026年AI精准获客TOP5技巧,让您的业务增长不再难 - 轩铭卿
  • 终极指南:5分钟快速上手layerdivider AI图像分层工具
  • 2026江苏价格合理短视频服务机构排行:5家实力品牌盘点 - 奔跑123
  • 【永磁同步电机】基于SVPWM的三电平逆变器PMSM速度控制附Simulink仿真
  • 终极Windows更新修复指南:如何快速解决95%的系统更新故障
  • 2026跨省寄大件哪家便宜?实测寄半折直击最低价 - 快递物流资讯
  • ELI5数据集:面向可解释长文本问答的开源基准
  • Java毕设选题推荐:基于 SpringBoot 的食材搭配与菜谱生成系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 职业资格证书都有什么?2026年职场人系统提升能力的进阶路径与方法
  • 珠海横琴管道疏通 TOP5 权威排行榜(2026 年 6 月专业版) - 园子一号
  • 【MATLAB集群控制代码(13)】多UAV分布式围捕,基于PID的编队控制仿真。不依赖任何集中调度节点,每架UAV仅凭自身传感器感知目标位置与自身状态,独立运行两路PID控制器完成围捕任务。
  • 2026年 青岛H5设计/李沧网站设计/页面设计/山东宣传册设计/海报设计/模板站设计推荐榜单:本土创意与视觉定制实力派盘点 - 品牌发掘
  • VMware Workstation Pro 17免费激活终极指南:5284个许可证密钥完整获取方案
  • 10分钟搭建一个AI Skill,新手也能学会
  • 2026年 餐饮手套/一次性食品手套/生鲜手套/烘焙手套/外卖打包手套厂家推荐:安全耐用与卫生标准之选 - 品牌发掘
  • i.MX25 ARM9车机芯片:入门级车载信息娱乐系统硬件设计与Linux开发实战
  • 2026年TOP5专业GEO服务公司排行,谁将引领行业新趋势? - 轩铭卿
  • 3个设计突破:为什么Bebas Neue正在重新定义免费字体体验?
  • 3分钟解锁网易云音乐NCM格式:ncmdump让你的音乐重获自由
  • Unity游戏马赛克移除技术深度解析:基于BepInEx插件框架的视觉优化方案
  • Claude手搓的IntelliJ Git扩展插件上线
  • Claude Fable 5调试bug展超强能力,AI编程智能体安全隐患引反思
  • 东莞搬家公司收费透明吗?了解这些细节避免陷阱 - 从来都是英雄出少年
  • EPPlus架构解析:构建企业级Excel处理引擎的工程实践
  • VC6环境下可直接编译运行的MFC图形化PING工具完整工程包
  • PostgreSQL 技术日报 (6月12日)|自研云原生 PG 平台,AI 开源共享协议发布