🎨 鸿蒙原生应用实战(十)ArkUI 涂鸦画板:Canvas 绘图 + 颜色选择 + 笔画管理 + 导出
博主说: 从儿童涂鸦到专业绘图,画板应用覆盖了各种用户群体。今天我们用 ArkUI 的 Canvas 2D API,从零实现一个支持自由手绘、颜色切换、笔画粗细、撤销重做、导出图片的完整涂鸦画板。
📱 应用场景
| 场景 | 说明 |
|---|---|
| ✏️ 随手涂鸦 | 用手指在屏幕上画画 |
| 📝 课堂笔记 | 用手写笔做批注 |
| 🖼️ 图片标注 | 截图后标记重点 |
| 🧒 儿童绘画 | 彩色画笔自由创作 |
⚙️ 运行环境要求
| 项目 | 版本要求 |
|---|---|
| DevEco Studio | 5.0.3.800+ |
| HarmonyOS SDK | API 12 |
| 核心 API | Canvas 2D + @ohos.multimedia.image |
| 权限 | 无特殊权限 |
🛠️ 实战:从零搭建涂鸦画板
Step 1:画板核心架构
触摸事件 (PanGesture)
↓ 记录轨迹点
路径列表 (paths: PathData[])
↓ 逐个绘制到 Canvas
画布渲染
↓
撤销: 删除最后一条路径
重做: 恢复删除的路径
清除: 清空所有路径
导出: ImagePacker 编码为图片
Step 2:完整代码
typescript
// pages/Index.ets --- 涂鸦画板
import image from '@ohos.multimedia.image';
import fileIo from '@ohos.file.fs';
interface Point { x: number; y: number; }
interface StrokeData {
points: Point[]; // 轨迹点
color: string; // 颜色
width: number; // 粗细
opacity: number; // 透明度
}
@Entry
@Component
struct DoodlePad {
private ctx!: CanvasRenderingContext2D;
@State strokes: StrokeData[] = [];
@State undoneStrokes: StrokeData[] = [];
@State currentColor: string = '#007AFF';
@State currentWidth: number = 4;
@State currentOpacity: number = 1;
@State currentPoints: Point[] = [];
@State isDrawing: boolean = false;
@State brushType: 'pen' | 'marker' | 'eraser' = 'pen';
@State canvasWidth: number = 360;
@State canvasHeight: number = 500;
private colors: string[] = ['#FF3B30','#FF9500','#FFCC00','#34C759','#007AFF','#5856D6','#AF52DE','#000000','#888888','#FFFFFF'];
private widths: number[] = [2, 4, 8, 12, 20];
private canvasUpdateId: number = 0;
// ======== 开始绘制 ========
onDrawStart(event: GestureEvent) {
this.isDrawing = true;
const x = event.fingerInfo[0]?.x || 0;
const y = event.fingerInfo[0]?.y || 0;
this.currentPoints = [{ x, y }];
// 绘制起点
this.ctx.beginPath();
this.ctx.arc(x, y, this.currentWidth / 2, 0, Math.PI * 2);
this.ctx.fillStyle = this.brushType === 'eraser' ? '#FFFFFF' : this.currentColor;
this.ctx.fill();
}
// ======== 绘制中 ========
onDrawMove(event: GestureEvent) {
if (!this.isDrawing) return;
const x = event.fingerInfo[0]?.x || 0;
const y = event.fingerInfo[0]?.y || 0;
this.currentPoints.push({ x, y });
const prev = this.currentPoints[this.currentPoints.length - 2];
if (!prev) return;
this.ctx.beginPath();
this.ctx.moveTo(prev.x, prev.y);
this.ctx.lineTo(x, y);
this.ctx.strokeStyle = this.brushType === 'eraser' ? '#FFFFFF' : this.currentColor;
this.ctx.lineWidth = this.currentWidth;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.globalAlpha = this.brushType === 'eraser' ? 1 : this.currentOpacity;
this.ctx.stroke();
this.ctx.globalAlpha = 1;
}
// ======== 结束绘制 ========
onDrawEnd() {
if (this.currentPoints.length < 2) return;
this.strokes.push({
points: [...this.currentPoints],
color: this.brushType === 'eraser' ? '#FFFFFF' : this.currentColor,
width: this.currentWidth,
opacity: this.brushType === 'eraser' ? 1 : this.currentOpacity
});
this.currentPoints = [];
this.isDrawing = false;
this.undoneStrokes = []; // 新笔画清除重做栈
}
// ======== 撤销 ========
undo() {
if (this.strokes.length === 0) return;
const last = this.strokes.pop()!;
this.undoneStrokes.push(last);
this.redrawAll();
}
// ======== 重做 ========
redo() {
if (this.undoneStrokes.length === 0) return;
const stroke = this.undoneStrokes.pop()!;
this.strokes.push(stroke);
this.redrawAll();
}
// ======== 清除全部 ========
clearAll() {
this.undoneStrokes.push(...this.strokes);
this.strokes = [];
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
}
// ======== 重绘所有笔画 ========
redrawAll() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
for (const stroke of this.strokes) {
if (stroke.points.length < 2) continue;
this.ctx.beginPath();
this.ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
for (let i = 1; i < stroke.points.length; i++) {
this.ctx.lineTo(stroke.points[i].x, stroke.points[i].y);
}
this.ctx.strokeStyle = stroke.color;
this.ctx.lineWidth = stroke.width;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.globalAlpha = stroke.opacity;
this.ctx.stroke();
this.ctx.globalAlpha = 1;
}
}
// ======== 导出图片 ========
async exportImage() {
try {
// Canvas 转 PixelMap
const pixelMap = await this.ctx.getPixelMap(0, 0, this.canvasWidth, this.canvasHeight);
const packer = image.createImagePacker();
const packed = await packer.packing(pixelMap, { format: 'image/png', quality: 100 });
const path = getContext(this).filesDir + `/doodle_${Date.now()}.png`;
const file = fileIo.openSync(path, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, packed.data);
fileIo.closeSync(file);
AlertDialog.show({ message: `✅ 已保存到: ${path}` });
} catch (err) {
AlertDialog.show({ message: '导出失败: ' + JSON.stringify(err) });
}
}
// ======== UI 构建 ========
build() {
Column() {
// 顶部工具栏
Row() {
Button('↩').fontSize(18).backgroundColor('transparent').fontColor('#333')
.onClick(() => { this.undo(); })
Button('↪').fontSize(18).backgroundColor('transparent').fontColor('#333')
.onClick(() => { this.redo(); })
Button('🗑️').fontSize(16).backgroundColor('transparent').fontColor('#FF3B30')
.onClick(() => { this.clearAll(); })
Text('🎨').fontSize(20)
Button('📤').fontSize(16).backgroundColor('transparent').fontColor('#007AFF')
.onClick(() => { this.exportImage(); })
}
.width('100%').justifyContent(FlexAlign.SpaceEvenly).padding(8)
.backgroundColor('#F8F9FA')
// 画布
Canvas(this.ctx)
.width(this.canvasWidth).height(this.canvasHeight)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E0E0E0' })
.gesture(
PanGesture({ distance: 1 })
.onActionStart((e) => { this.onDrawStart(e); })
.onActionUpdate((e) => { this.onDrawMove(e); })
.onActionEnd(() => { this.onDrawEnd(); })
)
// 颜色选择器
Row() {
ForEach(this.colors, (color: string) => {
Circle().width(28).height(28)
.fill(color)
.stroke(this.currentColor === color ? '#333' : 'transparent')
.strokeWidth(3)
.onClick(() => { this.currentColor = color; this.brushType = 'pen'; })
})
}
.width('100%').justifyContent(FlexAlign.Center).gap(6).padding(8)
// 粗细 + 透明度
Row() {
ForEach(this.widths, (w: number) => {
Circle().width(Math.max(16, w * 2)).height(Math.max(16, w * 2))
.fill(this.currentWidth === w ? this.currentColor : '#ddd')
.onClick(() => { this.currentWidth = w; })
})
Text('🖊️').fontSize(20).onClick(() => { this.brushType = 'pen'; })
Text('🖌️').fontSize(20).onClick(() => { this.brushType = 'marker'; this.currentWidth = 12; })
Text('🧹').fontSize(20).onClick(() => { this.brushType = 'eraser'; this.currentWidth = 20; })
}
.width('100%').justifyContent(FlexAlign.Center).gap(8).padding({ bottom: 8 })
}
.width('100%').height('100%').backgroundColor('#fff')
}
}

官方文档: HarmonyOS 应用开发文档
- 开发者社区: 华为开发者论坛
- 欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net/