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

发布时间:2026/7/30 1:32:04
# 鸿蒙 HarmonyOS 应用开发实战(第39期)|手势绘制(Drawing Canvas)— onTouch 事件与画笔/橡皮擦 一、应用概述手势绘制Drawing Canvas是一款手指涂鸦画板应用。用户可以在画布上通过手指滑动自由绘制线条支持 8 种颜色选择和 3 种笔触大小切换并提供了橡皮擦擦除和清空画布功能。在当前版本的 HarmonyOS 中虽然没有像 Web 端 Canvas API 那样提供path或lineTo等绘图上下文但通过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)描边高亮。点击颜色时将currentColor设为该颜色将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组件被用作三个不同角色颜色选择器28×28 的色块圆绘制点动态生成的涂鸦点选中状态指示描边高亮同一组件在不同上下文中扮演不同视觉角色体现了组件复用的灵活性。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数组达到数千个点时性能可能下降。优化策略点采样只在Move事件中间隔采样每 3 个事件记录 1 个而不是每个事件都记录最大点数限制当点数超过阈值时移除最早的点离屏渲染使用Canvas组件如果可用替代点阵式渲染5.4 坐标偏移问题event.touches[0].x/y返回的是相对于被触摸组件的坐标。如果画布在页面中不是从 (0,0) 开始坐标可能需要调整。当前代码中画布是页面中居中的子组件坐标是相对于画布本身的因此不需要额外调整。六、可扩展思路Canvas 组件替代如果 HarmonyOS 版本支持 Canvas 组件可以使用 CanvasRenderingContext2D 绘图性能更好且支持线条平滑。撤销/重做维护一个操作历史栈支持 CtrlZ 撤销。保存到相册将画布内容导出为图片并保存到系统相册。线条平滑在触摸点之间进行插值如贝塞尔曲线使绘制线条更平滑。文字输入支持在画布上添加文字标签。七、总结手势绘制应用以 99 行代码实现了一个功能完整的涂鸦画板。它的技术亮点包括onTouch 事件处理Down 和 Move 事件的组合使用实现连续绘制点阵式绘图用 Circle position 模拟画笔绘制橡皮擦算法基于距离阈值的点移除算法工具切换画笔/橡皮擦的 toggle 模式切换颜色选择器ForEach 渲染的颜色圆形选择面板多级笔触4/8/16 三档笔触大小切换虽然当前代码存在点阵渲染缺失的问题但整个设计思路——触摸事件收集 数据驱动渲染——是完全正确的。这种模式可以扩展到签名板、笔记涂鸦、儿童绘画等各类手写应用中。下一期我们将进入一个更有趣的领域——弹弹球Bouncing Ball一个基于物理模拟的休闲小游戏看如何用代码实现碰撞检测和重力反弹效果。本文是 HarmonyOS 应用开发实战系列第 39 期更多内容请关注专栏。