欢迎加入开源鸿蒙 PC社区https://harmonypc.csdn.net/效果截图第5篇手写画布实现系列教程导航篇号标题状态01环境搭建与项目创建✅02数据模型与单词仓库✅03主入口页面与导航结构✅04极速划词页面实现✅05手写画布实现本篇06百度OCR手写识别接入下一篇07答案比对与反馈UI08单词切换与底部导航09词根分解与水印展示10项目总结与优化方向源码仓库https://atomgit.com/qq_33247427/englishProject一、Canvas 组件基础1.1 创建 CanvasArkUI 提供了Canvas组件用于 2D 绑图API 与 Web 标准的 Canvas 2D Context 基本一致// 1. 创建渲染上下文privatesettings:RenderingContextSettingsnewRenderingContextSettings(true);privatecanvasCtx:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings);// 2. 在 UI 中使用Canvas(this.canvasCtx).id(speedDictCanvas)// 必须设置 id后续截图需要.width(100%).height(100%).onReady((){// Canvas 准备就绪可以开始绑图this.setupBrush();}).onTouch((event:TouchEvent){// 处理触摸事件})1.2 RenderingContextSettingsnewRenderingContextSettings(true)// true 开启抗锯齿开启抗锯齿后线条边缘更平滑手写体验更好。1.3 Canvas 的 id 属性.id(speedDictCanvas)非常重要——后续使用componentSnapshot.get(speedDictCanvas)截图时需要通过这个 id 找到组件。二、画笔配置2.1 基本画笔设置setupBrush(){this.canvasCtx.strokeStyle#1A1A1A;// 笔色近黑色this.canvasCtx.lineWidth4;// 笔宽 4pxthis.canvasCtx.lineCapround;// 线条端点圆形this.canvasCtx.lineJoinround;// 线条连接处圆形}2.2 各属性效果对比属性值效果lineCapbutt方形端点默认lineCapround圆形端点推荐lineCapsquare方形但多出半个线宽lineJoinmiter尖角连接默认lineJoinround圆角连接推荐lineJoinbevel斜角连接对于手写场景roundround的组合最自然笔画起止和转折处都是圆润的。2.3 笔宽选择场景推荐笔宽说明英文单词3-5 px字母笔画较细中文汉字5-8 px笔画需要更粗签名2-3 px流畅细线三、触摸绑图核心防卡顿3.1 错误实现会越来越卡很多教程给出的写法// ❌ 错误写法.onTouch((event:TouchEvent){consttouchevent.touches[0];switch(event.type){caseTouchType.Down:this.canvasCtx.beginPath();this.canvasCtx.moveTo(touch.x,touch.y);break;caseTouchType.Move:this.canvasCtx.lineTo(touch.x,touch.y);this.canvasCtx.stroke();// 每次都重绘整条路径break;caseTouchType.Up:break;}})问题stroke()会绘制当前 path 中的所有线段。随着 Move 事件不断触发path 越来越长可能几百上千个点每次stroke()都要重绘从起点到当前点的所有线段导致帧率急剧下降。3.2 正确实现每段独立绘制// ✅ 正确写法.onTouch((event:TouchEvent){consttouchevent.touches[0];switch(event.type){caseTouchType.Down:this.isDrawingtrue;this.canvasCtx.beginPath();this.canvasCtx.moveTo(touch.x,touch.y);break;caseTouchType.Move:if(this.isDrawing){this.canvasCtx.lineTo(touch.x,touch.y);this.canvasCtx.stroke();// 关键重新开始路径this.canvasCtx.beginPath();this.canvasCtx.moveTo(touch.x,touch.y);}break;caseTouchType.Up:this.isDrawingfalse;break;}})原理每次 Move 后立即beginPath()moveTo()将当前点作为新路径的起点。这样每次stroke()只绘制最新的一小段线条从上一个点到当前点性能恒定不会随书写时间增长而变慢。3.3 性能对比方案书写 100 个点后的 stroke 开销书写 1000 个点后错误写法绘制 100 段线条绘制 1000 段线条正确写法绘制 1 段线条绘制 1 段线条四、三层 Stack 结构4.1 为什么需要三层默写单词时画布上需要同时展示白色背景— OCR 截图需要白底才能识别水印文字— 显示当前单词供参考手写笔迹— 用户的书写内容如果只用一个 CanvasCanvas 背景白色 → 水印被遮住Canvas 背景透明 → OCR 截图没有对比度解决方案三层 Stack 叠加。4.2 层级结构Stack({alignContent:Alignment.TopStart}){// 第 1 层白色底最底层Column().width(100%).height(100%).backgroundColor(#FFFFFF)// 第 2 层水印文字中间层if(this.showWatermarkthis.currentWord!null){Column({space:12}){Text(this.currentWord.english).fontSize(48).fontColor(#E8EDE0)// 极浅色.fontWeight(FontWeight.Bold).fontStyle(FontStyle.Italic)Text(this.currentWord.phonetic).fontSize(18).fontColor(#D0D8C8)}.width(100%).height(100%).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).hitTestBehavior(HitTestMode.Transparent)// 关键}// 第 3 层Canvas最顶层透明背景Canvas(this.canvasCtx).id(speedDictCanvas).width(100%).height(100%).backgroundColor(Color.Transparent)// 透明.onReady((){this.setupBrush();}).onTouch(...)}4.3 关键属性解析hitTestBehavior(HitTestMode.Transparent)水印层在 Canvas 下方但 Stack 中后面的子元素在上层。如果水印层拦截了触摸事件Canvas 就收不到了。HitTestMode.Transparent让触摸事件穿透水印层传递给下方的 Canvas。等等——Canvas 在最顶层Stack 中最后声明的在最上面水印在中间所以触摸事件先到 Canvas不需要穿透。那为什么还要加hitTestBehavior答防止水印层在某些情况下如条件渲染重新插入时意外拦截事件。加上这个属性是防御性编程。backgroundColor(Color.Transparent)Canvas 默认背景是白色。设为透明后Canvas 只显示笔迹水印文字从下层透出来。4.4 视觉效果用户看到的效果 ┌─────────────────────────────┐ │ │ │ electrical │ ← 浅色水印可见 │ /ɪˈlektrɪkl/ │ │ │ │ ████████ │ ← 用户笔迹覆盖在水印上方 │ ████ │ │ │ └─────────────────────────────┘五、清空画布5.1 clearCanvas 方法clearCanvas(){constwthis.canvasCtx.width;consththis.canvasCtx.height;this.canvasCtx.clearRect(0,0,w,h);// 清除所有像素this.setupBrush();// 重新设置画笔this.feedbackText;// 清除反馈this.showAnswerfalse;}5.2 为什么不用 fillRect 填白因为 Canvas 背景是透明的clearRect会将像素清除为透明水印自然从下层显示出来。如果用fillRect(#FFFFFF)填白会遮住水印。六、componentSnapshot 截图6.1 截图原理componentSnapshot.get(id)会将指定 id 的组件及其所有子层渲染为一张 PixelMap 图片。由于我们的 Stack 包含白底 水印 笔迹截图结果是一张白底上有笔迹的图片水印颜色极浅不影响 OCR。import{componentSnapshot}fromkit.ArkUI;import{image}fromkit.ImageKit;constpixelMap:image.PixelMapawaitcomponentSnapshot.get(speedDictCanvas);6.2 注意事项截图是异步操作需要await截图分辨率取决于设备像素密度高分屏可能很大截图包含 Canvas 的透明背景——但因为 Stack 下面有白底层最终截图是白底的七、水印开关7.1 状态控制StateshowWatermark:booleantrue;// 在工具栏中Row({space:6}){Text(水印).fontSize(12).fontColor(#6B7280)Toggle({type:ToggleType.Switch,isOn:this.showWatermark}).selectedColor(#8B9D6B).width(40).height(22).onChange((isOn:boolean){this.showWatermarkisOn;})}7.2 条件渲染if(this.showWatermarkthis.currentWord!null){// 渲染水印}当showWatermark为 false 时水印层不渲染用户看到的是纯白画布。八、画布容器样式Stack(...).layoutWeight(1)// 占满剩余垂直空间.width(100%).backgroundColor(#FFFFFF).borderRadius(4)// 微圆角.clip(true)// 裁剪超出内容.border({width:1,color:#E5E7EB})borderRadius(4)微圆角不要太圆保持工具感clip(true)笔迹画到边缘时不会溢出整个 DictationContent 有padding({ left: 30, right: 30 })画布距离页面边缘 30px九、本篇小结通过本篇教程我们完成了理解了 Canvas 组件的创建和配置掌握了画笔属性设置lineCap、lineJoin、lineWidth解决了画笔卡顿问题每段独立 stroke实现了三层 Stack 结构白底 水印 透明画布理解了 hitTestBehavior 触摸穿透机制实现了清空画布和水印开关了解了 componentSnapshot 截图原理下一篇预告第 6 篇百度 OCR 手写识别接入— 我们将把画布截图发送到百度手写文字识别 API获取识别结果实现写完即识别的核心功能。