ArkUI 涂鸦画板:Canvas 绘图 + 颜色选择 + 笔画管理 + 导出)
鸿蒙原生应用实战十ArkUI 涂鸦画板Canvas 绘图 颜色选择 笔画管理 导出博主说从儿童涂鸦到专业绘图画板应用覆盖了各种用户群体。今天我们用 ArkUI 的 Canvas 2D API从零实现一个支持自由手绘、颜色切换、笔画粗细、撤销重做、导出图片的完整涂鸦画板。 应用场景场景说明✏️ 随手涂鸦用手指在屏幕上画画 课堂笔记用手写笔做批注️ 图片标注截图后标记重点 儿童绘画彩色画笔自由创作⚙️ 运行环境要求项目版本要求DevEco Studio5.0.3.800HarmonyOS SDKAPI 12核心 APICanvas 2D ohos.multimedia.image权限无特殊权限️ 实战从零搭建涂鸦画板Step 1画板核心架构触摸事件 (PanGesture) ↓ 记录轨迹点 路径列表 (paths: PathData[]) ↓ 逐个绘制到 Canvas 画布渲染 ↓ 撤销: 删除最后一条路径 重做: 恢复删除的路径 清除: 清空所有路径 导出: ImagePacker 编码为图片Step 2完整代码// pages/Index.ets — 涂鸦画板importimagefromohos.multimedia.image;importfileIofromohos.file.fs;interfacePoint{x:number;y:number;}interfaceStrokeData{points:Point[];// 轨迹点color:string;// 颜色width:number;// 粗细opacity:number;// 透明度}EntryComponentstruct DoodlePad{privatectx!:CanvasRenderingContext2D;Statestrokes:StrokeData[][];StateundoneStrokes:StrokeData[][];StatecurrentColor:string#007AFF;StatecurrentWidth:number4;StatecurrentOpacity:number1;StatecurrentPoints:Point[][];StateisDrawing:booleanfalse;StatebrushType:pen|marker|eraserpen;StatecanvasWidth:number360;StatecanvasHeight:number500;privatecolors:string[][#FF3B30,#FF9500,#FFCC00,#34C759,#007AFF,#5856D6,#AF52DE,#000000,#888888,#FFFFFF];privatewidths:number[][2,4,8,12,20];privatecanvasUpdateId:number0;// 开始绘制 onDrawStart(event:GestureEvent){this.isDrawingtrue;constxevent.fingerInfo[0]?.x||0;constyevent.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.fillStylethis.brushTypeeraser?#FFFFFF:this.currentColor;this.ctx.fill();}// 绘制中 onDrawMove(event:GestureEvent){if(!this.isDrawing)return;constxevent.fingerInfo[0]?.x||0;constyevent.fingerInfo[0]?.y||0;this.currentPoints.push({x,y});constprevthis.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.strokeStylethis.brushTypeeraser?#FFFFFF:this.currentColor;this.ctx.lineWidththis.currentWidth;this.ctx.lineCapround;this.ctx.lineJoinround;this.ctx.globalAlphathis.brushTypeeraser?1:this.currentOpacity;this.ctx.stroke();this.ctx.globalAlpha1;}// 结束绘制 onDrawEnd(){if(this.currentPoints.length2)return;this.strokes.push({points:[...this.currentPoints],color:this.brushTypeeraser?#FFFFFF:this.currentColor,width:this.currentWidth,opacity:this.brushTypeeraser?1:this.currentOpacity});this.currentPoints[];this.isDrawingfalse;this.undoneStrokes[];// 新笔画清除重做栈}// 撤销 undo(){if(this.strokes.length0)return;constlastthis.strokes.pop()!;this.undoneStrokes.push(last);this.redrawAll();}// 重做 redo(){if(this.undoneStrokes.length0)return;conststrokethis.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(conststrokeofthis.strokes){if(stroke.points.length2)continue;this.ctx.beginPath();this.ctx.moveTo(stroke.points[0].x,stroke.points[0].y);for(leti1;istroke.points.length;i){this.ctx.lineTo(stroke.points[i].x,stroke.points[i].y);}this.ctx.strokeStylestroke.color;this.ctx.lineWidthstroke.width;this.ctx.lineCapround;this.ctx.lineJoinround;this.ctx.globalAlphastroke.opacity;this.ctx.stroke();this.ctx.globalAlpha1;}}// 导出图片 asyncexportImage(){try{// Canvas 转 PixelMapconstpixelMapawaitthis.ctx.getPixelMap(0,0,this.canvasWidth,this.canvasHeight);constpackerimage.createImagePacker();constpackedawaitpacker.packing(pixelMap,{format:image/png,quality:100});constpathgetContext(this).filesDir/doodle_${Date.now()}.png;constfilefileIo.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.currentColorcolor?#333:transparent).strokeWidth(3).onClick((){this.currentColorcolor;this.brushTypepen;})})}.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.currentWidthw?this.currentColor:#ddd).onClick((){this.currentWidthw;})})Text(️).fontSize(20).onClick((){this.brushTypepen;})Text(️).fontSize(20).onClick((){this.brushTypemarker;this.currentWidth12;})Text().fontSize(20).onClick((){this.brushTypeeraser;this.currentWidth20;})}.width(100%).justifyContent(FlexAlign.Center).gap(8).padding({bottom:8})}.width(100%).height(100%).backgroundColor(#fff)}}官方文档HarmonyOS 应用开发文档开发者社区华为开发者论坛欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net/