# 鸿蒙 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 绘图,性能更好且支持线条平滑。
- 撤销/重做:维护一个操作历史栈,支持 Ctrl+Z 撤销。
- 保存到相册:将画布内容导出为图片并保存到系统相册。
- 线条平滑:在触摸点之间进行插值(如贝塞尔曲线),使绘制线条更平滑。
- 文字输入:支持在画布上添加文字标签。
七、总结
手势绘制应用以 99 行代码,实现了一个功能完整的涂鸦画板。它的技术亮点包括:
- onTouch 事件处理:Down 和 Move 事件的组合使用实现连续绘制
- 点阵式绘图:用 Circle + position 模拟画笔绘制
- 橡皮擦算法:基于距离阈值的点移除算法
- 工具切换:画笔/橡皮擦的 toggle 模式切换
- 颜色选择器:ForEach 渲染的颜色圆形选择面板
- 多级笔触:4/8/16 三档笔触大小切换
虽然当前代码存在点阵渲染缺失的问题,但整个设计思路——触摸事件收集 + 数据驱动渲染——是完全正确的。这种模式可以扩展到签名板、笔记涂鸦、儿童绘画等各类手写应用中。
下一期我们将进入一个更有趣的领域——弹弹球(Bouncing Ball),一个基于物理模拟的休闲小游戏,看如何用代码实现碰撞检测和重力反弹效果。
本文是 HarmonyOS 应用开发实战系列第 39 期,更多内容请关注专栏。
更多推荐



所有评论(0)