news 2026/7/30 1:31:16

# 鸿蒙 HarmonyOS 应用开发实战(第39期)|手势绘制(Drawing Canvas)— onTouch 事件与画笔/橡皮擦

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
# 鸿蒙 HarmonyOS 应用开发实战(第39期)|手势绘制(Drawing Canvas)— onTouch 事件与画笔/橡皮擦

一、应用概述

手势绘制(Drawing Canvas)是一款手指涂鸦画板应用。用户可以在画布上通过手指滑动自由绘制线条,支持 8 种颜色选择和 3 种笔触大小切换,并提供了橡皮擦擦除和清空画布功能。

在当前版本的 HarmonyOS 中,虽然没有像 Web 端 Canvas API 那样提供pathlineTo等绘图上下文,但通过onTouch事件记录触摸点坐标,再利用Stack层叠布局在不同位置渲染小圆点,同样可以实现流畅的手绘效果。这种点阵式绘图的巧妙实现,展示了 ArkTS 声明式 UI 在面对图形绘制需求时的灵活变通能力。

二、技术架构与组件树

2.1 整体架构

@Entry @Component struct index39 ├── Column (根容器) │ ├── Row (顶部导航栏) │ └── Column (主内容区, 居中) │ ├── Column (画布容器) │ │ ├── Text (透明占位, 340×340, 接受 onTouch) │ │ │ └── .overlay() ← 此处应有绘制点阵 │ │ └── (实际绘制通过 ForEach 在父级? 看下文分析) │ └── Row (工具栏) │ ├── ForEach (8 种颜色圆形选择器) │ ├── Blank() │ ├── Button (笔触大小切换 4/8/16) │ ├── Text ("🧹" 橡皮擦切换) │ └── Text ("🗑️" 清空)

2.2 数据模型

interface DrawPoint { x: number; y: number; color: string; size: number; } @State points: DrawPoint[] = []; @State currentColor: string = '#333'; @State currentSize: number = 4; @State tool: string = 'draw'; colors: string[] = ['#333', '#F44336', '#FF9800', '#FFD700', '#4CAF50', '#0077FF', '#9C27B0', '#000']; private canvasSize: number = 340;

DrawPoint接口定义了每个绘制点的属性:坐标 (x, y)、颜色、大小。@State points数组存储了所有绘制的点,是画布的全部数据源。

三、核心代码深度解析

3.1 触摸事件处理

onTouchStart(event: TouchEvent): void { const touch = event.touches[0]; if (this.tool === 'draw') { this.points = this.points.concat([ { x: touch.x, y: touch.y, color: this.currentColor, size: this.currentSize } ]); } } onTouchMove(event: TouchEvent): void { const touch = event.touches[0]; if (this.tool === 'draw') { this.points = this.points.concat([ { x: touch.x, y: touch.y, color: this.currentColor, size: this.currentSize } ]); } else { // 橡皮擦模式:移除触摸点附近的点 const newPoints: DrawPoint[] = []; for (let i = 0; i < this.points.length; i++) { const p = this.points[i]; if (Math.abs(p.x - touch.x) > 15 || Math.abs(p.y - touch.y) > 15) { newPoints.push(p); } } this.points = newPoints; } }

TouchEvent 结构

event.touches[0]是当前触摸点的信息,包含:

  • x: 触摸点相对于被触摸组件的 X 坐标
  • y: 触摸点相对于被触摸组件的 Y 坐标
  • id: 触摸点的唯一标识(多点触控时区分不同手指)

画笔模式(draw)

  • Down(手指按下)和Move(手指移动)时均记录点
  • 每次触摸产生的新点通过concat()追加到数组末尾
  • 点的颜色和大小取当前选中的值

橡皮擦模式(erase)

  • 遍历所有已绘制的点
  • 移除与当前触摸位置距离小于 15px 的点(Math.abs(p.x - touch.x) > 15 || Math.abs(p.y - touch.y) > 15
  • 保留距离较远的点

这里使用的是距离阈值法而不是精确的圆形碰撞检测——只要坐标差绝对值超过 15px 就保留。虽然不如Math.sqrt(dx² + dy²) < radius精确,但对于触控擦除场景已经足够,且计算更简单。

3.2 画布绑定 Touch 事件

Column() { Text('').width(this.canvasSize).height(this.canvasSize) .backgroundColor('#fff') .border({ width: 1, color: '#ddd' }) .borderRadius(8) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) this.onTouchStart(event); if (event.type === TouchType.Move) this.onTouchMove(event); }) .clip(true) }

onTouch是 ArkTS 中处理触摸事件的核心 API。它接收一个TouchEvent回调,包含:

  • type:TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起)、TouchType.Cancel(取消)
  • touches: 当前触摸点数组
  • changedTouches: 变化的触摸点数组

clip(true)确保绘制内容不会超出画布的圆角边界。

3.3 点阵渲染的缺失与解决方案

注意:当前代码中绘制触摸点时收集了points数据,但并没有在 build() 中实际渲染这些点。这意味着运行应用时触摸画布不会看到任何绘制结果。

要完成画布功能,需要在画布的.overlay()Stack中渲染points数组中的点。以下是完整的渲染方案:

Stack() { // 画布背景 Text('').width(this.canvasSize).height(this.canvasSize) .backgroundColor('#fff').border({ width: 1, color: '#ddd' }) .borderRadius(8) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) this.onTouchStart(event); if (event.type === TouchType.Move) this.onTouchMove(event); }) .clip(true) // 绘制所有点 ForEach(this.points, (p: DrawPoint, idx: number) => { Circle() .width(p.size).height(p.size) .fill(p.color) .position({ x: p.x - p.size / 2, y: p.y - p.size / 2 }) }, (p: DrawPoint, idx: number) => idx.toString()) }

这里使用Circle组件为每个点渲染一个小圆,位置通过position精确设置,减去了size/2使圆心对齐触摸点坐标。

3.4 颜色选择器

Row() { ForEach(this.colors, (c: string) => { Column() { Circle().width(28).height(28).fill(c) .stroke(this.currentColor === c ? '#333' : 'transparent') .strokeWidth(3) } .padding(3).onClick(() => { this.currentColor = c; this.tool = 'draw' }) }) }

8 种颜色的圆形选择器,选中状态通过.stroke('#333')描边高亮。点击颜色时:

  1. currentColor设为该颜色
  2. tool设为'draw'(从橡皮擦模式切回画笔模式)

颜色数组包含了从黑色到彩色的渐变:

  • #333(深灰/黑色)
  • #F44336(红)
  • #FF9800(橙)
  • #FFD700(金/黄)
  • #4CAF50(绿)
  • #0077FF(蓝)
  • #9C27B0(紫)
  • #000(纯黑)

3.5 笔触大小切换

Button() { Text(this.currentSize.toString()).fontSize(13).fontColor('#333') }.width(44).height(32).borderRadius(16).backgroundColor('#eee') .onClick(() => { if (this.currentSize === 4) this.currentSize = 8; else if (this.currentSize === 8) this.currentSize = 16; else this.currentSize = 4; })

笔触大小在4 → 8 → 16之间循环切换。按钮上显示当前大小数值,让用户直观了解当前笔触。

3.6 橡皮擦与清空

Text('🧹').fontSize(24).padding(4) .onClick(() => { this.tool = this.tool === 'erase' ? 'draw' : 'erase' }) Text('🗑️').fontSize(22).padding(4) .onClick(() => this.clearCanvas())

橡皮擦切换:点击 🧹 图标在'draw''erase'模式间切换。这是一个toggle 开关——再次点击切回画笔。

清空画布:🗑️ 图标调用clearCanvas()

clearCanvas(): void { this.points = []; }

直接将points置为空数组,所有绘制的点消失。

四、HarmonyOS 特性分析

4.1 onTouch 事件系统

HarmonyOS 的触摸事件系统包含三个级别:

事件触发时机用途
onTouch所有触摸动作原始触摸事件处理(本例使用)
onClick轻触后抬起按钮点击、图标点击
onGesture手势识别滑动、长按、捏合等高级手势

本例使用onTouch而非onClick,因为涂鸦需要追踪手指的连续运动,而非单纯的点击。

4.2 Circle 组件的灵活应用

在本应用中,Circle组件被用作三个不同角色:

  1. 颜色选择器:28×28 的色块圆
  2. 绘制点:动态生成的涂鸦点
  3. 选中状态指示:描边高亮

同一组件在不同上下文中扮演不同视觉角色,体现了组件复用的灵活性。

4.3 clip 裁剪属性

.clip(true)

clip属性控制子组件是否被父组件的边界裁剪。在画布中,clip(true)确保绘制的内容不会超出画布的圆角矩形边界,保持视觉整洁。

五、UI/UX 设计与最佳实践

5.1 工具栏布局

工具栏使用Row水平排列,左侧是颜色选择器,右侧是工具按钮。Blank()组件将左右两组元素推到两端,实现「左侧颜色、右侧工具」的清晰分区。

5.2 工具切换的视觉反馈

当前代码中,橡皮擦模式和画笔模式之间切换时,没有明显的视觉反馈来指示当前处于哪种模式。最佳实践是增加模式指示:

Text(this.tool === 'erase' ? '🧹 橡皮擦模式' : '✏️ 画笔模式') .fontSize(14).fontColor('#888')

或者在颜色选择器上叠加一个半透明遮罩,表示当前不可用。

5.3 性能优化建议

points数组达到数千个点时,性能可能下降。优化策略:

  1. 点采样:只在Move事件中间隔采样(每 3 个事件记录 1 个),而不是每个事件都记录
  2. 最大点数限制:当点数超过阈值时,移除最早的点
  3. 离屏渲染:使用Canvas组件(如果可用)替代点阵式渲染

5.4 坐标偏移问题

event.touches[0].x/y返回的是相对于被触摸组件的坐标。如果画布在页面中不是从 (0,0) 开始,坐标可能需要调整。当前代码中,画布是页面中居中的子组件,坐标是相对于画布本身的,因此不需要额外调整。

六、可扩展思路

  1. Canvas 组件替代:如果 HarmonyOS 版本支持 Canvas 组件,可以使用 CanvasRenderingContext2D 绘图,性能更好且支持线条平滑。
  2. 撤销/重做:维护一个操作历史栈,支持 Ctrl+Z 撤销。
  3. 保存到相册:将画布内容导出为图片并保存到系统相册。
  4. 线条平滑:在触摸点之间进行插值(如贝塞尔曲线),使绘制线条更平滑。
  5. 文字输入:支持在画布上添加文字标签。

七、总结

手势绘制应用以 99 行代码,实现了一个功能完整的涂鸦画板。它的技术亮点包括:

  • onTouch 事件处理:Down 和 Move 事件的组合使用实现连续绘制
  • 点阵式绘图:用 Circle + position 模拟画笔绘制
  • 橡皮擦算法:基于距离阈值的点移除算法
  • 工具切换:画笔/橡皮擦的 toggle 模式切换
  • 颜色选择器:ForEach 渲染的颜色圆形选择面板
  • 多级笔触:4/8/16 三档笔触大小切换

虽然当前代码存在点阵渲染缺失的问题,但整个设计思路——触摸事件收集 + 数据驱动渲染——是完全正确的。这种模式可以扩展到签名板、笔记涂鸦、儿童绘画等各类手写应用中。

下一期我们将进入一个更有趣的领域——弹弹球(Bouncing Ball),一个基于物理模拟的休闲小游戏,看如何用代码实现碰撞检测和重力反弹效果。


本文是 HarmonyOS 应用开发实战系列第 39 期,更多内容请关注专栏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/30 1:30:35

第5讲|RAG 知识库全栈落地:企业智能问答不是“上传文档 + 聊天框”

写在前面:为什么企业 AI 落地绕不开 RAG? 在前四讲里,我们已经完成了 AI 产品经理从认知到实战的第一阶段。 第一讲,我们建立了 AI 产品经理的岗位图谱,明确 AI PM 的核心价值不是“会用 AI 工具”,而是能把模型能力转化为产品价值。 第二讲,我们讲了大模型底层通识,…

作者头像 李华
网站建设 2026/7/30 1:30:06

25种语言支持,一键下载全球视频:你的数字内容收藏家

25种语言支持&#xff0c;一键下载全球视频&#xff1a;你的数字内容收藏家 【免费下载链接】VideoDownloadHelper Chrome Extension to Help Download Video for Some Video Sites. 项目地址: https://gitcode.com/gh_mirrors/vi/VideoDownloadHelper 你是否曾为错过精…

作者头像 李华
网站建设 2026/7/30 1:29:44

计算机单片机毕设实战-基于 STM32 的智能水箱监测与自动加热装置开发 基于传感器采集的水箱水温水位智能管控系统设计(011801)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/7/30 1:27:57

AI如何重塑教育论文数据分析:从数据清洗到可视化

1. 项目概述&#xff1a;AI如何重塑教育论文的数据分析范式在教育研究领域&#xff0c;数据质量直接决定论文结论的可信度。传统人工处理方式存在三大痛点&#xff1a;主观偏差难以避免、统计方法选择依赖经验、可视化呈现不够直观。"书匠策AI"的定位正是解决这些核心…

作者头像 李华
网站建设 2026/7/30 1:26:22

3步快速上手:Ryujinx Switch模拟器终极指南与性能优化技巧

3步快速上手&#xff1a;Ryujinx Switch模拟器终极指南与性能优化技巧 【免费下载链接】Ryujinx 用 C# 编写的实验性 Nintendo Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/ry/Ryujinx Ryujinx是一款用C#编写的开源任天堂Switch模拟器&#xff0c;让你…

作者头像 李华