当前位置: 首页 > news >正文

鸿蒙原生应用实战(十)ArkUI 涂鸦画板:Canvas 绘图 + 颜色选择 + 笔画管理 + 导出

🎨 鸿蒙原生应用实战(十)ArkUI 涂鸦画板:Canvas 绘图 + 颜色选择 + 笔画管理 + 导出

博主说:从儿童涂鸦到专业绘图,画板应用覆盖了各种用户群体。今天我们用 ArkUI 的 Canvas 2D API,从零实现一个支持自由手绘、颜色切换、笔画粗细、撤销重做、导出图片的完整涂鸦画板


📱 应用场景

场景说明
✏️ 随手涂鸦用手指在屏幕上画画
📝 课堂笔记用手写笔做批注
🖼️ 图片标注截图后标记重点
🧒 儿童绘画彩色画笔自由创作

⚙️ 运行环境要求

项目版本要求
DevEco Studio5.0.3.800+
HarmonyOS SDKAPI 12
核心 APICanvas 2D +@ohos.multimedia.image
权限无特殊权限

🛠️ 实战:从零搭建涂鸦画板

Step 1:画板核心架构

触摸事件 (PanGesture) ↓ 记录轨迹点 路径列表 (paths: PathData[]) ↓ 逐个绘制到 Canvas 画布渲染 ↓ 撤销: 删除最后一条路径 重做: 恢复删除的路径 清除: 清空所有路径 导出: ImagePacker 编码为图片

Step 2:完整代码

// pages/Index.ets — 涂鸦画板importimagefrom'@ohos.multimedia.image';importfileIofrom'@ohos.file.fs';interfacePoint{x:number;y:number;}interfaceStrokeData{points:Point[];// 轨迹点color:string;// 颜色width:number;// 粗细opacity:number;// 透明度}@Entry@Componentstruct DoodlePad{privatectx!:CanvasRenderingContext2D;@Statestrokes:StrokeData[]=[];@StateundoneStrokes:StrokeData[]=[];@StatecurrentColor:string='#007AFF';@StatecurrentWidth:number=4;@StatecurrentOpacity:number=1;@StatecurrentPoints:Point[]=[];@StateisDrawing:boolean=false;@StatebrushType:'pen'|'marker'|'eraser'='pen';@StatecanvasWidth:number=360;@StatecanvasHeight:number=500;privatecolors:string[]=['#FF3B30','#FF9500','#FFCC00','#34C759','#007AFF','#5856D6','#AF52DE','#000000','#888888','#FFFFFF'];privatewidths:number[]=[2,4,8,12,20];privatecanvasUpdateId:number=0;// ======== 开始绘制 ========onDrawStart(event:GestureEvent){this.isDrawing=true;constx=event.fingerInfo[0]?.x||0;consty=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;constx=event.fingerInfo[0]?.x||0;consty=event.fingerInfo[0]?.y||0;this.currentPoints.push({x,y});constprev=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;constlast=this.strokes.pop()!;this.undoneStrokes.push(last);this.redrawAll();}// ======== 重做 ========redo(){if(this.undoneStrokes.length===0)return;conststroke=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(conststrokeofthis.strokes){if(stroke.points.length<2)continue;this.ctx.beginPath();this.ctx.moveTo(stroke.points[0].x,stroke.points[0].y);for(leti=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;}}// ======== 导出图片 ========asyncexportImage(){try{// Canvas 转 PixelMapconstpixelMap=awaitthis.ctx.getPixelMap(0,0,this.canvasWidth,this.canvasHeight);constpacker=image.createImagePacker();constpacked=awaitpacker.packing(pixelMap,{format:'image/png',quality:100});constpath=getContext(this).filesDir+`/doodle_${Date.now()}.png`;constfile=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/
http://www.rkmt.cn/news/1522904.html

相关文章:

  • 如何5分钟掌握免费离线OCR工具Umi-OCR:隐私安全与高效识别全指南
  • 实数编码遗传算法工程实践:从收敛失效到稳定优化
  • 2026怀化大众首选贵金属回收商户名录 TOP 金条、铂金、白银线下回收门店信息一览 - 中业金奢再生回收中心
  • Windows右键菜单终极优化指南:ContextMenuManager让系统操作效率翻倍
  • 大模型不是省钱工具,而是成本重分配引擎
  • KMS_VL_ALL_AIO技术架构深度解析:开源激活引擎的设计与实现
  • 2026马鞍山全城黄金回收口碑商户盘点 TOP铂金回收白银回收旧料回收门店电话地址一览 - 信誉隆金银铂奢回收
  • 内存短缺致成本飙升,手机涨价趋势将持续到明年,促销季折扣或难寻
  • 点云压缩实战:对比MPEG G-PCC八叉树编码与Draco、PCL库的性能差异
  • 【趣解】你上网的全过程:从敲回车到看到网页
  • 北京西城区黄金回收今日行情与变现全攻略 - 专业黄金回收
  • Azure SQL数据库全生命周期管理:创建、销毁与成本治理实战
  • CefFlashBrowser:终极Flash内容访问与存档管理解决方案
  • macOS窗口自动提升神器:AutoRaise让你的鼠标悬停更智能
  • LenovoLegionToolkit启动异常:WMI通信故障诊断与硬件接口修复指南
  • GRACE数据中断别慌:SSA插值 vs. 传统方法,我们实测对比了效果
  • 别再傻傻分不清了!STM32驱动EC11编码器,一定位一脉冲和两定位一脉冲到底怎么选?
  • 2026丽水房屋安全鉴定权威机构排行 TOP危房鉴定 + 结构检测 + 抗震安全评估 实地测评整理 电话地址 - 鉴安检测
  • Java解析DXF文件,除了Kabeja这个2008年的老库,我们还有别的选择吗?
  • 文件路径操作的艺术:Python的Pathlib模块详解
  • GPT4ALL的LocalDocs功能实战:如何把你的PDF和TXT文档变成私人知识库(Python调用指南)
  • 2026沈阳市民高频光顾的 5 家线下黄金回收白银铂金回收实体店实地走访测评 - 中安检金银铂钻回收
  • 拆解IEEE TII/TITS/IoTJ:从投稿要求到审稿内幕,你的论文到底适合投哪家?
  • Java开发者如何安全合规地试用Aspose.CAD 21.11?聊聊官方试用与替代方案
  • 2026益阳本地贵金属变现门店精选前五+黄金铂金白银金条回收合规商家名录 含地址电话 - 诚金汇钻回收公司
  • AList项目易主后,我的私人云存储方案还安全吗?聊聊替代品与风险规避
  • 2026防城港大众首选贵金属回收商户名录 TOP 金条、铂金、白银线下回收门店信息一览 - 中业金奢再生回收中心
  • 2026焦作全城黄金回收口碑商户盘点 TOP铂金回收白银回收旧料回收门店电话地址一览 - 信誉隆金银铂奢回收
  • 哔哩下载姬DownKyi:你的B站视频下载终极免费方案
  • 2026果洛房屋安全鉴定权威机构排行 TOP危房鉴定 + 结构检测 + 抗震安全评估 实地测评整理 电话地址 - 鉴安检测