手把手教你写个涂鸦板:鸿蒙原生开发初体验
这篇文章摘要(150字): 本文详细介绍了如何使用鸿蒙系统开发一个简单的涂鸦板应用。文章从创建项目开始,逐步讲解了核心功能实现:通过Canvas API绘制线条、处理触摸事件实现实时绘图、使用状态管理保存绘制记录。重点展示了撤销/重做功能的双栈实现原理,以及UI布局设计。教程采用手把手方式,包含完整的代码片段和关键点说明,适合鸿蒙开发初学者快速入门Canvas绘图和手势交互开发。文章强调状态管理的
手把手教你写个涂鸦板:鸿蒙原生开发初体验
嘿,朋友们!今天咱们来搞点有意思的——用鸿蒙写一个涂鸦板!
别被"鸿蒙原生开发"这几个字吓到,其实没那么难。跟着我的节奏走,保证你能跑起来一个能画画的小应用。
咱们要做什么?
就是一个简单的涂鸦板:
- 手指在屏幕上滑,就能画出线条
- 可以选8种颜色
- 可以调笔刷粗细(细、中、粗三档)
- 支持撤销和重做
- 能清空画布
- 还能切换深色/浅色背景
听起来功能挺多的?别急,咱们一步步来。
第一步:先把项目建起来
打开 DevEco Studio(华为官方的 IDE,写鸿蒙应用必备),新建项目:
选 Empty Ability 模板,这个最干净,啥都没有,正好咱们从头写。
填项目信息:
- Project name:
MyApplication(名字随便起) - Bundle name:
com.example.myapplication - Compatible SDK:
6.1.0(23) - Target SDK:
6.1.1(24)
等 Gradle 同步完,项目就建好了。看一下目录结构:
MyApplication/
├── AppScope/
│ └── app.json5 # 应用配置
├── entry/ # 主模块
│ └── src/main/
│ ├── ets/ # 代码在这写
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets # 入口
│ │ └── pages/
│ │ └── Index.ets # 主页面(重点!)
│ └── resources/ # 资源文件
└── build-profile.json5
咱们主要就改一个文件:entry/src/main/ets/pages/Index.ets
第二步:理解一下核心逻辑
涂鸦板的原理其实很简单:
- 用户手指按下去 → 记录起点
- 手指移动 → 一边记录新点,一边画线
- 手指抬起 → 把这条线保存起来
所以咱们需要存:
- 所有的线条(每条线是一堆点的集合)
- 每条线的颜色和粗细
用代码描述就是:
interface Point {
x: number
y: number
}
interface DrawAction {
points: Point[] // 这条线的所有点
color: string // 颜色
width: number // 粗细
}
第三步:开始写代码
3.1 状态定义
打开 Index.ets,先定义咱们需要的状态变量:
@Entry
@Component
struct Index {
// Canvas 上下文,绑定画布的
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D()
// 所有画过的线
@State lines: DrawAction[] = []
// 撤销栈(重做用)
@State undoStack: DrawAction[] = []
// 当前颜色和粗细
@State currentColor: string = '#333333'
@State currentWidth: number = 4
// 正在画的标记
@State isDrawing: boolean = false
@State bgWhite: boolean = true // 背景色
// 当前这条线(临时的)
private currentLine: Point[] = []
}
@State 这个装饰器很重要!加上它,变量变了 UI 就会跟着变。不加?那你改了数据界面也不动。
3.2 画线的方法
drawLine(action: DrawAction): void {
const ctx = this.context
if (action.points.length < 2) return // 点太少画不了
ctx.beginPath()
ctx.lineCap = 'round' // 线头圆的
ctx.lineJoin = 'round' // 拐角圆的
ctx.strokeStyle = action.color
ctx.lineWidth = action.width
// 从第一个点开始,连到后面所有点
ctx.moveTo(action.points[0].x, action.points[0].y)
for (let i = 1; i < action.points.length; i++) {
ctx.lineTo(action.points[i].x, action.points[i].y)
}
ctx.stroke()
}
这就是 Canvas 的经典画线套路:moveTo 起点 → lineTo 下一个点 → stroke() 真正画出来。
3.3 处理触摸事件
这是重点!三个事件要处理:
// 手指按下
handleTouchStart(event: TouchEvent): void {
const touch = event.touches[0]
this.isDrawing = true
this.currentLine = [{ x: touch.x, y: touch.y }]
}
// 手指移动
handleTouchMove(event: TouchEvent): void {
if (!this.isDrawing) return
const touch = event.touches[0]
this.currentLine.push({ x: touch.x, y: touch.y })
// 实时画最新的一段(性能优化)
const points = this.currentLine
if (points.length < 2) return
const ctx = this.context
const last = points[points.length - 1]
const prev = points[points.length - 2]
ctx.beginPath()
ctx.strokeStyle = this.currentColor
ctx.lineWidth = this.currentWidth
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.moveTo(prev.x, prev.y)
ctx.lineTo(last.x, last.y)
ctx.stroke()
}
// 手指抬起
handleTouchEnd(): void {
if (!this.isDrawing || this.currentLine.length < 2) {
this.isDrawing = false
this.currentLine = []
return
}
// 保存这条线
const action: DrawAction = {
points: [...this.currentLine],
color: this.currentColor,
width: this.currentWidth
}
this.lines = [...this.lines, action] // 注意!要重新赋值才能触发更新
this.undoStack = [] // 新操作清空重做栈
this.isDrawing = false
this.currentLine = []
}
小技巧:在 handleTouchMove 里,咱们只画最新的一段,不重绘所有线。这样画得快,不卡顿。
第四步:撤销和重做
这个用两个栈实现,经典套路:
handleUndo(): void {
if (this.lines.length === 0) return
const last = this.lines[this.lines.length - 1]
this.undoStack = [...this.undoStack, last]
this.lines = this.lines.slice(0, -1)
this.redrawAll()
}
handleRedo(): void {
if (this.undoStack.length === 0) return
const last = this.undoStack[this.undoStack.length - 1]
this.lines = [...this.lines, last]
this.undoStack = this.undoStack.slice(0, -1)
this.redrawAll()
}
撤销 = 把最后一条线从主栈移到撤销栈
重做 = 从撤销栈拿回来
第五步:搞定 UI 界面
5.1 整体布局
build() {
Column() {
// 顶部工具栏
// ...
// 画布
Stack() {
Canvas(this.context)
.width('100%')
.height(400)
.backgroundColor(this.bgWhite ? '#FFFFFF' : '#212121')
.borderRadius(12)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.handleTouchStart(event)
} else if (event.type === TouchType.Move) {
this.handleTouchMove(event)
} else if (event.type === TouchType.Up) {
this.handleTouchEnd()
}
})
}
// 颜色选择
// ...
// 粗细选择
// ...
}
.width('100%')
.height('100%')
.backgroundColor('#FFF8E1')
}
5.2 颜色选择器
private readonly colors: string[] = [
'#333333', '#E53935', '#FB8C00', '#FDD835',
'#43A047', '#1E88E5', '#8E24AA', '#FFFFFF'
]
Row() {
ForEach(this.colors, (color: string) => {
Stack() {
Circle()
.width(32).height(32)
.fill(color)
.stroke(color === '#FFFFFF' ? '#DDD' : color)
if (color === this.currentColor) {
Circle()
.width(38).height(38)
.fill('none')
.stroke('#5D4037')
.strokeWidth(2.5)
}
}
.onClick(() => { this.currentColor = color })
}, (color: string) => color)
}
5.3 粗细选择
private readonly widths: number[] = [3, 6, 12]
private readonly widthLabels: string[] = ['细', '中', '粗']
Row() {
ForEach(this.widths, (width: number, index: number) => {
Column() {
Circle()
.width(width * 2)
.height(width * 2)
.fill(this.currentColor)
Text(this.widthLabels[index])
.fontSize(12)
.fontColor(this.currentWidth === width ? '#5D4037' : '#BBB')
}
.backgroundColor(this.currentWidth === width ? '#EFEBE9' : 'transparent')
.onClick(() => { this.currentWidth = width })
}, (width: number) => width.toString())
}
第六步:深色模式
一键切换背景色:
toggleBg(): void {
this.bgWhite = !this.bgWhite
this.redrawAll()
}
踩坑提醒
坑一:数组改了界面不动
// ❌ 这样不行
this.lines.push(action)
// ✅ 要这样
this.lines = [...this.lines, action]
ArkTS 的响应式要求你给数组一个新的引用,直接 push 不触发更新。
坑二:白色按钮看不清
白色圆形在浅色背景下几乎看不见,加个灰色边框:
.stroke(color === '#FFFFFF' ? '#DDD' : color)
坑三:撤销后再画,重做丢了
这是正常的!新操作清空撤销栈是正确行为,不然重做就乱了。

总结
这篇教程带你从零写了个涂鸦板,核心就是:
- Canvas 画布 + CanvasRenderingContext2D
- 触摸事件三件套:Down、Move、Up
- @State 响应式更新
- 栈结构实现撤销/重做
入门鸿蒙,这个小项目挺合适的。有问题评论区见!
原创不易,转载请注明出处 👋
更多推荐



所有评论(0)