鸿蒙实战:手写板 WritePage 触摸绘图实现
鸿蒙实战手写板 WritePage 触摸绘图实现前言必读图鸿蒙实战手写板 WritePage 触摸绘图实现第 79 篇 运行效果截图HarmonyOS NEXT手写板让用户可以直接用手指在屏幕上书写无需纸笔。本项目通过 Canvas 2D API 触摸事件处理实现了一个完整的手写板触摸绘图 → 笔画历史管理 → 撤销/清空 → 分段进度驱动。本文从触摸事件体系到手写板实现逐层解析。图WritePage 手写板架构——触摸事件处理、Canvas 绘制与笔画历史管理WritePage触摸事件监听Canvas 绘图引擎笔画历史管理TouchType.DownbeginPath / moveToTouchType.MovelineTo / strokeTouchType.UpclosePath / 保存笔画strokeStyle 笔色lineWidth 笔宽lineCap roundlinJoin round撤销 undorestoreImage清空 clearclearRect保存 savetoDataURL→PixelMap一、触摸事件基础1.1 TouchEvent API.onTouch((e:TouchEvent){// e.type — 触摸事件类型// e.touches[0] — 触摸点数组// e.touches[0].x — 触摸点 X 坐标// e.touches[0].y — 触摸点 Y 坐标})事件类型触发时机举例TouchType.Down手指接触屏幕手指按下TouchType.Move手指在屏幕上移动手指拖动TouchType.Up手指离开屏幕手指抬起TouchType.Cancel触摸被系统中断来电/通知1.2 CanvasRenderingContext2D APIcontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(newRenderingContextSettings(true))// 核心绘制方法ctx.beginPath()// 开始新路径ctx.moveTo(x,y)// 移动到起点ctx.lineTo(x,y)// 画线到终点ctx.stroke()// 描边ctx.clearRect(0,0,w,h)// 清空画布// 样式属性ctx.strokeStyle#3D352E// 描边颜色ctx.lineWidth3// 线宽ctx.lineCapround// 线帽圆角ctx.lineJoinround// 连接点圆角二、WritePage 完整实现2.1 数据结构interfaceDrawPoint{x:numbery:number}// 笔画历史DrawPoint[][] — 每一笔是一个 DrawPoint[] 数组// history[0] stroke 1 的所有触摸点// history[1] stroke 2 的所有触摸点// ...privatehistory:DrawPoint[][][]privatecurrentPoints:DrawPoint[][]// 当前正在绘制的笔画privateisDrawing:booleanfalse// 是否正在绘制2.2 触摸事件三件套// Touch Down开始记录privatetouchStart(e:TouchEvent):void{this.isDrawingtruethis.currentPoints[this.touchPoint(e)]}// Touch Move实时绘制privatetouchMove(e:TouchEvent):void{if(!this.isDrawing)returnthis.currentPoints.push(this.touchPoint(e))this.drawCurrentStroke()}// Touch Up完成一笔privatetouchEnd():void{this.isDrawingfalseif(this.currentPoints.length0){this.history.push([...this.currentPoints])// 保存到历史this.currentPoints[]// 清空当前// 自动推进分段进度if(this.segmentsWrittenthis.totalSegments){this.segmentsWritten}}}privatetouchPoint(e:TouchEvent):DrawPoint{return{x:e.touches[0].x,y:e.touches[0].y}}2.3 实时绘制与重绘// 实时绘制Touch Move 时调用privatedrawCurrentStroke():void{constctxthis.context ctx.beginPath()ctx.moveTo(this.currentPoints[0].x,this.currentPoints[0].y)for(leti1;ithis.currentPoints.length;i){ctx.lineTo(this.currentPoints[i].x,this.currentPoints[i].y)}ctx.strokeStyle#3D352Ectx.lineWidth3ctx.lineCaproundctx.lineJoinroundctx.stroke()}// 完全重绘撤销/清空后调用privateredraw():void{constctxthis.context ctx.clearRect(0,0,this.canvasWidth,this.canvasHeight)for(conststrokeofthis.history){ctx.beginPath()stroke.forEach((p,i){if(i0)ctx.moveTo(p.x,p.y)elsectx.lineTo(p.x,p.y)})ctx.strokeStyle#3D352Ectx.lineWidth3ctx.lineCaproundctx.lineJoinroundctx.stroke()}}三、撤销与清空// 撤销上一笔privateundo():void{if(this.history.length0){this.history.pop()// 移除最后一笔this.redraw()// 完全重绘if(this.segmentsWritten0)this.segmentsWritten--// 回退分段}}// 清空全部privateclear():void{this.history[]// 清空历史this.currentPoints[]// 清空当前this.segmentsWritten0// 重置分段constctxthis.context ctx.clearRect(0,0,this.canvasWidth,this.canvasHeight)// 清空画布}四、Canvas 尺寸自适应Canvas(this.context).width(100%).height(100%).onAreaChange((oldValue:Area,newValue:Area){// 获取 Canvas 实际宽高用于 clearRectthis.canvasWidthnewValue.widthasnumberthis.canvasHeightnewValue.heightasnumber}).onReady((){// Canvas 就绪后重绘恢复历史笔画this.redraw()})生命周期事件触发时机用途onAreaChange组件位置/尺寸变化获取 Canvas 实际宽高onReadyCanvas 初始化完成首次重绘五、底部工具栏// Builder 封装的可复用按钮BuilderToolBtn(symbol:string,label:string,onTap:()void){Column(){Text(symbol).fontSize(18).fontColor(AppColors.TEXT_2).width(44).height(44).borderRadius(22).backgroundColor(AppColors.CARD).shadow({...}).onClick(onTap)Text(label).fontSize(10).fontColor(AppColors.TEXT_3).margin({top:6})}}// 调用Row(){this.ToolBtn(↩,撤销,()this.undo())Blank().layoutWeight(1)this.ToolBtn(,清空,()this.clear())Blank().layoutWeight(1)this.ToolBtn(✎,粗细,(){showToast({message:粗细切换开发中,duration:1000})})}六、触摸绘图的性能优化// 优化 1Touch Move 不要每帧都重新绘制整张画布// ❌ 错误每次 Touch Move 都 clearRect 全量 redraw// ✅ 正确只绘制新增线段// 优化 2限制采样频率可选privatelastSampleTime:number0privatetouchMove(e:TouchEvent):void{if(!this.isDrawing)returnconstnowDate.now()if(now-this.lastSampleTime16)return// 60fps 限制this.lastSampleTimenowthis.currentPoints.push(this.touchPoint(e))this.drawCurrentStroke()}// 优化 3Canvas 尺寸与设备像素比匹配// 避免模糊constscale2// 2x 渲染canvas.widththis.canvasWidth*scale canvas.heightthis.canvasHeight*scale ctx.scale(scale,scale)七、手写数据流向WritePage 用户书写 ↓ State segmentsWritten 3 三段写完 ↓ 用户点击「完成」 ↓ router.back() 返回首页 ↓ 捕获 Canvas 为图片 → 与拍照流程合并 ↓ pushUrl(AnalyzingPage) → OCR → 特征提取 → 情绪分类八、WritePage vs 其他绘制方案方案适用场景优点缺点Canvas (本项目)手写绘图轻量、触摸事件原生支持笔画端点锯齿需 roundSVG矢量图形高清放大触摸事件需额外处理自定义组件简单标记易实现不适合连续笔画九、注意事项与常见问题9.1 开发注意事项在正式开发前建议按以下步骤完成环境准备与前置检查版本确认检查 DevEco Studio 与 SDK 版本确保满足目标 API Level 要求权限声明在module.json5的requestPermissions字段中提前声明所有需要的系统权限设备能力检查调用前验证设备是否支持目标能力相机、NFC、传感器等异步封装所有耗时操作数据库、文件 I/O、网络请求统一使用async/await处理资源释放在组件aboutToDisappear()生命周期钩子中及时释放系统资源防止内存泄漏9.2 常见错误与解决方案常见问题快速排查表问题类型排查方向参考方法应用崩溃查看 hilog 错误日志hilog.error(TAG, ..., e.message)状态丢失检查 AppStorage 键名拼写统一使用常量管理键名动画不流畅避免在 animateTo 回调中执行 I/O动画与数据操作分离总结✅触摸事件三件套Touch Down开始/ Move绘制/ Up完成并保存✅Canvas 2D 绘制beginPath → moveTo → lineTo → stroke 标准管线✅笔画历史管理DrawPoint[][]存储每一笔的所有触摸点✅撤销/清空pop 历史 redraw 全量重绘✅Canvas 尺寸自适应onAreaChange onReady 生命周期✅性能优化仅绘制新增线段、60fps 采样限制、像素比适配✅数据流向手写 → Canvas 图片 → 分析流水线下一篇将深入讲解分段书写引导与进度设计。 收藏提示触摸绘图的关键是三件套时刻——Touch Down 记录起点、Move 实时绘制、Up 保存笔画到历史。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源Canvas CanvasRenderingContext2DTouchEvent 触摸事件onTouch 手势处理animateTo 显式动画HarmonyOS NEXT 开发指南第44篇 Canvas 组件基础第61篇 交互反馈动画第50篇 Canvas 动画七、手写板高级功能实现7.1 贝塞尔曲线平滑笔画直接用lineTo绘制触摸点会产生折线感使用贝塞尔曲线可以平滑笔画// 平滑笔画核心算法privatedrawSmoothLine(points:TouchPoint[]){if(points.length2)return;constctxthis.ctx;ctx.beginPath();ctx.moveTo(points[0].x,points[0].y);for(leti1;ipoints.length-1;i){// 取相邻两点的中间点作为控制点constmidX(points[i].xpoints[i1].x)/2;constmidY(points[i].ypoints[i1].y)/2;ctx.quadraticCurveTo(points[i].x,points[i].y,midX,midY);}ctx.stroke();}7.2 可变压感模拟根据触摸点的移动速度动态调整线宽模拟真实书写压感privategetLineWidth(speed:number):number{// 速度快 → 线细速度慢 → 线粗模拟压感constmaxWidth8;constminWidth2;constnormalizedSpeedMath.min(speed/20,1);returnmaxWidth-normalizedSpeed*(maxWidth-minWidth);}// 计算速度constspeedMath.sqrt(dx*dxdy*dy);this.ctx.lineWidththis.getLineWidth(speed);7.3 分段书写的进度管理书写板支持分段书写每次写一段通过进度追踪实现StatecurrentSegment:number1;// 当前书写段落StatetotalSegments:number4;// 总段落数StatesegmentImages:string[][];// 每段保存的图片路径privateasyncfinishSegment(){// 保存当前段落constpixelMapawaitthis.canvasContext.getPixelMap(0,0,this.width,this.height);constpathawaitFileUtil.savePixelMap(pixelMap,segment_${this.currentSegment});this.segmentImages.push(path);if(this.currentSegmentthis.totalSegments){// 全部完成跳转分析router.pushUrl({url:pages/AnalyzingPage,params:{segments:this.segmentImages}});}else{// 清空画板准备下一段this.ctx.clearRect(0,0,this.width,this.height);this.currentSegment;}}如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力