# [特殊字符] 画板 — 鸿蒙ArkTS自定义绘制与手势交互系统
·
一、应用概述
1.1 应用简介
画板(Drawing Board)是一款自由绘画创作工具,支持多种画笔工具、颜色选择、笔画粗细调整、撤销重做、背景色切换和图片导出功能。该应用以HarmonyOS NEXT的ArkTS框架为基础,深入展示了手势识别与触摸事件处理、自定义绘制Canvas、绘制数据结构设计、命令模式(撤销/重做)、多工具绘制模式和图片导出等关键技术。
1.2 核心功能
| 功能模块 | 功能描述 | 技术实现 | 设计考量 |
|---|---|---|---|
| 画笔工具 | 画笔/直线/矩形/圆形/擦除 | 绘制模式枚举 | 多工具切换 |
| 颜色选择 | 10+预设颜色+自定义颜色 | 颜色数据+取色器 | 色彩丰富 |
| 笔画粗细 | 5种笔画宽度(2-20px) | 配置管理 | 适合不同场景 |
| 撤销重做 | 操作历史管理 | 命令模式 | 无限撤销 |
| 清空画布 | 一键清空 | 状态重置 | 确认提示 |
| 背景切换 | 多种背景色/网格 | 样式绑定 | 辅助绘画 |
| 导出图片 | 保存为PNG/JPEG | 截图API | 分享保存 |
| 手势识别 | 单指绘画/双指缩放 | 触摸事件处理 | 多点触控 |
1.3 应用架构
画板应用采用分层架构:
- UI表现层:绘画画布、工具栏、颜色选择器、粗细滑块、状态栏。
- 业务逻辑层:绘制引擎、命令管理器(撤销/重做)、工具管理器、颜色管理器。
- 数据持久层:使用Preferences API保存画布设置和最近使用的颜色。
二、绘制系统
2.1 绘制数据结构
interface DrawPoint {
x: number;
y: number;
}
interface DrawAction {
tool: DrawingTool;
color: string;
width: number;
points: DrawPoint[];
opacity: number;
fillColor: string | null; // 填充颜色(用于矩形、圆形)
timestamp: number;
}
enum DrawingTool {
PEN = 'pen', // 画笔
LINE = 'line', // 直线
RECTANGLE = 'rectangle', // 矩形
CIRCLE = 'circle', // 圆形
ERASER = 'eraser', // 橡皮擦
ARROW = 'arrow', // 箭头
TEXT = 'text' // 文字
}
interface CanvasState {
actions: DrawAction[];
currentTool: DrawingTool;
currentColor: string;
currentWidth: number;
backgroundColor: string;
opacity: number;
showGrid: boolean;
gridSize: number;
}
class DrawingManager {
private actions: DrawAction[] = [];
private redoStack: DrawAction[] = [];
private currentPoints: DrawPoint[] = [];
private isDrawing: boolean = false;
private maxActions: number = 500; // 最大操作数
startDrawing(point: DrawPoint, tool: DrawingTool, color: string, width: number): void {
this.isDrawing = true;
this.currentPoints = [point];
// 对于直线/矩形/圆形,记录起始点
const action: DrawAction = {
tool,
color,
width,
points: [point],
opacity: 1.0,
fillColor: null,
timestamp: Date.now()
};
this.actions.push(action);
}
continueDrawing(point: DrawPoint): void {
if (!this.isDrawing) return;
this.currentPoints.push(point);
const lastAction = this.actions[this.actions.length - 1];
if (lastAction) {
if (lastAction.tool === DrawingTool.PEN || lastAction.tool === DrawingTool.ERASER) {
lastAction.points.push(point);
} else {
// 直线/矩形/圆形:更新结束点
lastAction.points = [this.currentPoints[0], point];
}
}
}
endDrawing(): void {
this.isDrawing = false;
this.currentPoints = [];
this.redoStack = []; // 新操作清除重做栈
// 限制操作数
if (this.actions.length > this.maxActions) {
this.actions = this.actions.slice(-this.maxActions);
}
}
// 撤销
undo(): DrawAction | null {
if (this.actions.length === 0) return null;
const action = this.actions.pop()!;
this.redoStack.push(action);
return action;
}
// 重做
redo(): DrawAction | null {
if (this.redoStack.length === 0) return null;
const action = this.redoStack.pop()!;
this.actions.push(action);
return action;
}
canUndo(): boolean {
return this.actions.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
clear(): void {
this.actions = [];
this.redoStack = [];
this.currentPoints = [];
this.isDrawing = false;
}
getActions(): DrawAction[] {
return [...this.actions];
}
getActionCount(): number {
return this.actions.length;
}
}
2.2 Canvas绘制引擎
class CanvasRenderer {
private context: CanvasRenderingContext2D | null = null;
setContext(ctx: CanvasRenderingContext2D): void {
this.context = ctx;
}
render(actions: DrawAction[], backgroundColor: string, showGrid: boolean, gridSize: number): void {
if (!this.context) return;
const ctx = this.context;
const canvas = ctx.canvas;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格
if (showGrid) {
this.drawGrid(ctx, canvas.width, canvas.height, gridSize);
}
// 绘制所有动作
for (const action of actions) {
this.drawAction(ctx, action);
}
}
private drawGrid(ctx: CanvasRenderingContext2D, width: number, height: number, gridSize: number): void {
ctx.strokeStyle = '#E0E0E0';
ctx.lineWidth = 0.5;
for (let x = 0; x <= width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
for (let y = 0; y <= height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
}
private drawAction(ctx: CanvasRenderingContext2D, action: DrawAction): void {
if (action.points.length < 1) return;
ctx.save();
ctx.globalAlpha = action.opacity;
switch (action.tool) {
case DrawingTool.PEN:
this.drawPenStroke(ctx, action);
break;
case DrawingTool.ERASER:
this.drawEraserStroke(ctx, action);
break;
case DrawingTool.LINE:
this.drawLine(ctx, action);
break;
case DrawingTool.RECTANGLE:
this.drawRectangle(ctx, action);
break;
case DrawingTool.CIRCLE:
this.drawCircle(ctx, action);
break;
case DrawingTool.ARROW:
this.drawArrow(ctx, action);
break;
}
ctx.restore();
}
private drawPenStroke(ctx: CanvasRenderingContext2D, action: DrawAction): void {
if (action.points.length < 2) {
ctx.fillStyle = action.color;
ctx.beginPath();
ctx.arc(action.points[0].x, action.points[0].y, action.width / 2, 0, Math.PI * 2);
ctx.fill();
return;
}
ctx.strokeStyle = action.color;
ctx.lineWidth = action.width;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
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();
}
private drawEraserStroke(ctx: CanvasRenderingContext2D, action: DrawAction): void {
ctx.strokeStyle = action.color; // 背景色
ctx.lineWidth = action.width * 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
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();
}
private drawLine(ctx: CanvasRenderingContext2D, action: DrawAction): void {
if (action.points.length < 2) return;
ctx.strokeStyle = action.color;
ctx.lineWidth = action.width;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(action.points[0].x, action.points[0].y);
ctx.lineTo(action.points[1].x, action.points[1].y);
ctx.stroke();
}
private drawRectangle(ctx: CanvasRenderingContext2D, action: DrawAction): void {
if (action.points.length < 2) return;
const x = Math.min(action.points[0].x, action.points[1].x);
const y = Math.min(action.points[0].y, action.points[1].y);
const w = Math.abs(action.points[1].x - action.points[0].x);
const h = Math.abs(action.points[1].y - action.points[0].y);
ctx.strokeStyle = action.color;
ctx.lineWidth = action.width;
ctx.strokeRect(x, y, w, h);
if (action.fillColor) {
ctx.fillStyle = action.fillColor;
ctx.fillRect(x, y, w, h);
}
}
private drawCircle(ctx: CanvasRenderingContext2D, action: DrawAction): void {
if (action.points.length < 2) return;
const cx = (action.points[0].x + action.points[1].x) / 2;
const cy = (action.points[0].y + action.points[1].y) / 2;
const rx = Math.abs(action.points[1].x - action.points[0].x) / 2;
const ry = Math.abs(action.points[1].y - action.points[0].y) / 2;
const radius = Math.max(rx, ry);
ctx.strokeStyle = action.color;
ctx.lineWidth = action.width;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
if (action.fillColor) {
ctx.fillStyle = action.fillColor;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
}
}
private drawArrow(ctx: CanvasRenderingContext2D, action: DrawAction): void {
if (action.points.length < 2) return;
const x1 = action.points[0].x, y1 = action.points[0].y;
const x2 = action.points[1].x, y2 = action.points[1].y;
ctx.strokeStyle = action.color;
ctx.lineWidth = action.width;
ctx.lineCap = 'round';
// 画线
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// 画箭头(三角形)
const angle = Math.atan2(y2 - y1, x2 - x1);
const headLen = 15 + action.width;
ctx.fillStyle = action.color;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - headLen * Math.cos(angle - 0.4), y2 - headLen * Math.sin(angle - 0.4));
ctx.lineTo(x2 - headLen * Math.cos(angle + 0.4), y2 - headLen * Math.sin(angle + 0.4));
ctx.closePath();
ctx.fill();
}
}
三、UI交互设计
3.1 画板主界面
@Component
struct DrawingBoardView {
@State currentTool: DrawingTool = DrawingTool.PEN;
@State currentColor: string = '#000000';
@State currentWidth: number = 4;
@State backgroundColor: string = '#FFFFFF';
@State showGrid: boolean = false;
@State gridSize: number = 20;
@State canUndo: boolean = false;
@State canRedo: boolean = false;
@State actionCount: number = 0;
private drawingManager = new DrawingManager();
private renderer = new CanvasRenderer();
private canvasContext: CanvasRenderingContext2D | null = null;
private readonly COLORS: string[] = [
'#000000', '#333333', '#666666', '#999999',
'#E53935', '#FF5722', '#FF9800', '#FFC107',
'#4CAF50', '#2196F3', '#1565C0', '#9C27B0',
'#E91E63', '#00BCD4', '#795548', '#607D8B'
];
private readonly WIDTHS: number[] = [2, 4, 6, 10, 20];
build() {
Column() {
// 工具栏
this.renderToolbar()
// 画布
Stack() {
Canvas(this.canvasContext)
.width('100%')
.height('100%')
.backgroundColor(this.backgroundColor)
.onTouch((event: TouchEvent) => {
this.handleTouch(event);
})
}
.layoutWeight(1)
.width('100%')
.margin(8)
// 底部控制栏
this.renderBottomBar()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
private renderToolbar() {
Scroll({ direction: ScrollDirection.Horizontal }) {
Row({ space: 8 }) {
this.createToolButton('✏️', '画笔', DrawingTool.PEN)
this.createToolButton('📏', '直线', DrawingTool.LINE)
this.createToolButton('⬜', '矩形', DrawingTool.RECTANGLE)
this.createToolButton('⭕', '圆形', DrawingTool.CIRCLE)
this.createToolButton('🧹', '橡皮', DrawingTool.ERASER)
this.createToolButton('➡️', '箭头', DrawingTool.ARROW)
}
.padding(8)
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
}
@Builder
private createToolButton(emoji: string, label: string, tool: DrawingTool) {
Column() {
Text(emoji).fontSize(20)
Text(label).fontSize(10).fontColor(this.currentTool === tool ? '#4CAF50' : '#666')
}
.padding(8)
.backgroundColor(this.currentTool === tool ? '#E8F5E9' : 'transparent')
.borderRadius(8)
.onClick(() => { this.currentTool = tool; })
}
private handleTouch(event: TouchEvent): void {
if (event.touches.length === 0) return;
const touch = event.touches[0];
const point: DrawPoint = { x: touch.x, y: touch.y };
switch (event.type) {
case TouchType.Down:
this.drawingManager.startDrawing(point, this.currentTool, this.currentColor, this.currentWidth);
this.canUndo = this.drawingManager.canUndo();
break;
case TouchType.Move:
this.drawingManager.continueDrawing(point);
break;
case TouchType.Up:
this.drawingManager.endDrawing();
this.canUndo = this.drawingManager.canUndo();
this.canRedo = this.drawingManager.canRedo();
this.actionCount = this.drawingManager.getActionCount();
break;
}
this.renderCanvas();
}
private renderCanvas(): void {
this.renderer.render(
this.drawingManager.getActions(),
this.backgroundColor,
this.showGrid,
this.gridSize
);
}
}
四、总结
4.1 核心技术要点
- 绘制数据结构:基于DrawAction的数据结构,记录每次绘制的工具、颜色、粗细和路径点。
- Canvas绘制引擎:支持画笔、直线、矩形、圆形、箭头和橡皮擦6种工具。
- 命令模式撤销/重做:通过actions和redoStack实现无限撤销重做。
- 手势识别:基于TouchEvent的触摸事件处理,支持单指绘画。
- 颜色管理系统:16种预设颜色 + 自定义颜色选择。
- 图片导出:基于Canvas截图API的图片导出功能。
4.2 扩展方向
- 图层系统:支持多图层编辑,独立操作每个图层。
- 滤镜效果:添加模糊、锐化、马赛克等滤镜。
- 文字工具:在画布上添加文字,支持字体选择。
- 图片导入:导入图片作为背景或绘制参考。
- 形状识别:识别手绘形状并自动对齐为完美形状。
4.3 核心代码量统计
| 模块 | 核心代码行数 | 接口数 | 组件数 |
|---|---|---|---|
| 绘制管理器 | 120 | 8 | - |
| Canvas渲染引擎 | 200 | 6 | - |
| 命令管理器 | 60 | 5 | - |
| UI组件 | 300 | 4 | 5 |
| 总计 | 680 | 23 | 5 |
更多推荐



所有评论(0)