在 HarmonyOS 6.1.1 的维修签名页,我为何让画布重新绘制

📅 2026/8/20 23:53:06
在 HarmonyOS 6.1.1 的维修签名页,我为何让画布重新绘制
维修签字页最容易被低估的地方是把它理解成“在画布上画几条线”。实际接入时画面上同时存在几类不同性质的数据手指或触控笔刚落下时的当前笔画、已经完成的笔画、历史记录中的只读笔迹以及页面提示的本地确认状态。它们看起来都会落在同一块 Canvas 上却不能用同一条写入路径处理。我在这个场景里遇到的表象很简单点了撤销、清空、历史预览、退出预览或抗锯齿开关后页面文字已经变了签名板却可能仍然保留旧笔迹。继续书写时用户会不确定自己是在补写当前签名还是意外改动一条历史记录点“确认并归档”后又容易把本地页面记录误认为真实工单已经回写。所以我没有把“重新绘制”当成一个补丁而是把它放到每次状态变更之后。这里的重绘只做一件事用当前允许展示的笔迹重新生成 Canvas 像素。它能证明页面已按本地状态更新不能证明签名已提交到外部系统、真实工单已经归档或任意设备上的笔迹效果完全一致。先把问题拆成四层在维修签名场景里先分清数据所在的层比直接追某个按钮更重要。层次页面中的数据可以确认不能据此确认触摸输入activeStroke、触点坐标当前触摸正在被页面采集签名已有效、已确认或已保存新建签名strokes已采集的本地笔画可参与绘制已回写真实工单或已持久化历史预览signatureHistory、selectedHistoryId指定历史记录可被选中并只读展示历史记录来自真实服务端页面状态signatureStatus、viewMode、antialiasEnabled当前页面给出的本地提示和展示模式后端、设备或业务流程已经完成这四层都可能触发视觉变化但不能互相替代。例如signatureStatus写着“本地确认已记录”只能说明当前组件更新了状态文案signatureHistory多了一条记录才能说明页面内存中新增了一条本地历史项两者都不能推出真实工单已收到签名。把证据层拆开后面的每一次重绘才不会被写成虚假的业务成功。场景解说此图应记录新建签名模式、签名板和历史列表的页面基线只能证明页面已展示不能证明已有有效签名或工单回写。先定重绘的唯一输入当前应当可见的笔迹这个页面有“新建”和“预览”两种模式。新建模式下Canvas 应显示当前用户写入的strokes预览模式下Canvas 必须改为显示被选中历史记录的strokes。如果绘制函数直接引用this.strokes那么进入历史预览后仍会画出新建签名页面的“历史预览”标签就成了一个只有文字变化的假状态。源码把这个选择收敛到visibleStrokes()private visibleStrokes(): Stroke[] { const record this.selectedHistory(); return this.viewMode preview record ? record.strokes : this.strokes; }这段代码能够证明渲染前会根据viewMode和已选历史记录在历史笔迹与当前笔迹之间作出本地选择。它不能证明record.strokes一定来自服务端也不能说明这些坐标已经被持久化页面里预置的历史项和本地确认新增的历史项都只是当前组件可展示的数据。我把它看作绘制层的“唯一入口”。触摸、撤销、清空和预览切换可以分别修改不同状态但redraw()不再判断业务动作本身只取visibleStrokes()。这样做的好处是排查时可以把问题分开若模式标签已变而画面没变先检查是否触发redraw()若已触发重绘却仍是错误笔迹再检查visibleStrokes()的选择条件而不是把所有怀疑堆到触摸事件上。一笔签名不是一次赋值而是一段触摸生命周期签名采集从handleSignatureTouch()进入。它先拒绝预览状态下的写入再按Down、Move、Up与Cancel处理同一条笔画private handleSignatureTouch(event: TouchEvent): void { if (this.viewMode preview) { this.signatureStatus 历史签名只读请先退出预览后新建签名; return; } const point this.pointFromTouch(event); if (event.type TouchType.Down point) { const stroke: Stroke { points: [point] }; this.activeStroke stroke; this.strokes [...this.strokes, stroke]; this.signatureStatus 正在采集现场签名…; this.redraw(); return; } if (event.type TouchType.Move point this.activeStroke) { this.activeStroke.points.push(point); this.strokes [...this.strokes]; this.redraw(); return; } if (event.type TouchType.Up || event.type TouchType.Cancel) { if (point this.activeStroke) { this.activeStroke.points.push(point); } this.activeStroke undefined; this.signatureStatus this.validStrokeCount() 0 ? 笔迹已采集可确认本地归档 : 笔迹过短请重新签字; this.redraw(); } }这段代码能确认三件事。第一按下时会创建一条包含起点的Stroke并将它同时设置为活动笔画和当前笔画列表的一员。第二移动事件把后续坐标追加到这条活动笔画中再通过this.strokes [...this.strokes]让页面状态重新参与渲染。第三抬起或取消时都会结束活动笔画并根据当前有效笔画数更新本地提示。它同样划出了两个边界。触摸事件被收到不等于笔迹已经有效Cancel发生时页面只是在本地结束本次采集不能把它解释为用户完成签字。另一个边界是坐标来源pointFromTouch()只取当前触点的x、y代码没有做压感、笔锋或身份认证因此不能把这块画布描述为具有电子签名法律效力的签署链路。为什么移动一次就要重绘一次Canvas 不是声明式文本组件。strokes数组更新后之前执行过的moveTo、lineTo和stroke不会自动根据新数组改写已经存在的像素。因此移动事件只追加坐标而没有redraw()时数据层的笔画会越来越长画布上却仍停在旧轮廓等到抬笔时才一次性绘制用户看到的是断续或滞后的反馈。页面的redraw()处理顺序是固定的先判断画布是否已经准备好再应用当前抗锯齿设置清除旧像素、铺设白底与签名基线最后按模式绘制visibleStrokes()。private redraw(): void { if (!this.canvasReady || this.canvasWidth 0 || this.canvasHeight 0) { return; } this.applyAntialias(); this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight); this.context.fillStyle #FFFFFF; this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight); this.context.strokeStyle #D5E0EA; this.context.lineWidth 1; this.context.beginPath(); this.context.moveTo(34, this.canvasHeight - 64); this.context.lineTo(this.canvasWidth - 34, this.canvasHeight - 64); this.context.stroke(); if (this.viewMode preview) { this.drawStrokes(this.context, this.visibleStrokes(), this.canvasWidth / 240, this.canvasHeight / 130, #172D40, 5); } else { this.drawStrokes(this.context, this.visibleStrokes(), 1, 1, #172D40, 5); } }这里“清除后完整重画”不是多余操作。撤销、清空和模式切换都可能使旧笔画不再属于当前可见集合。若只在新增笔画时继续lineTo就没有可靠方式擦掉最后一笔、整页笔迹或新建模式下残留的历史预览先清空再从visibleStrokes()重建才能让画布与当前状态一致。这段代码能够确认当前组件使用“状态是来源、Canvas 是投影”的方式工作。它不能确认渲染帧率、触控延迟或不同屏幕密度下的肉眼效果因为源码没有提供性能采样、真机记录或设备对照数据。页面运行后出现笔迹也只能证明本地 Canvas 已绘制这些点。笔迹如何真正落到 Canvas 上drawStrokes()并不保存业务数据它只把已经存在的坐标按给定比例转为线段。每一条少于两个点的笔画会被跳过以免“刚按下一个点”被错误画成一条线private drawStrokes(context: CanvasRenderingContext2D, strokes: Stroke[], scaleX: number, scaleY: number, color: string, lineWidth: number): void { context.strokeStyle color; context.lineWidth lineWidth; context.lineCap round; context.lineJoin round; strokes.forEach((stroke: Stroke) { if (stroke.points.length 2) { return; } context.beginPath(); context.moveTo(stroke.points[0].x * scaleX, stroke.points[0].y * scaleY); for (let index 1; index stroke.points.length; index 1) { context.lineTo(stroke.points[index].x * scaleX, stroke.points[index].y * scaleY); } context.stroke(); }); }lineCap round与lineJoin round说明页面选择圆形端点和圆形连接来呈现笔迹它们描述的是当前本地绘制样式不是对签名真实性或图像质量的认证。scaleX、scaleY则解释了预览与新建模式为何不能完全沿用同一坐标比例历史记录按 240×130 的基准坐标保存在主签名板预览时需要放大到当前画布尺寸新建笔迹还处于当前画布坐标中可以按 1:1 绘制。若忽略这一点历史预览通常会出现两种问题要么笔迹被挤在左上角要么通过错误比例拉伸到超出边界。这里的比例换算能证明坐标映射存在不能证明它已适配所有窗口尺寸需要在具体运行环境中观察onAreaChange后的实际宽高才能评价视觉效果。场景解说此图应记录选中历史记录后的只读预览、签名板笔迹及“历史预览”状态只能证明本地选中记录被重绘不能证明历史记录已从外部系统查询成功。历史预览必须禁止写入而不只是把按钮藏起来签名历史面板通过selectHistoryRecord()切换到预览模式private selectHistoryRecord(id: string): void { this.selectedHistoryId id; this.viewMode preview; this.activeStroke undefined; this.signatureStatus 正在预览历史签名笔迹已锁定; this.redraw(); }表面上它只做了四次赋值实际上缺一项都会留下歧义。selectedHistoryId指定预览对象viewMode preview让visibleStrokes()改取历史笔迹清空activeStroke避免上一次正在写的笔画继续保留为活动状态最后强制redraw()把旧画面替换为所选记录。仅改变列表高亮或标题文字不足以完成“切换预览”。真正的保护并不在按钮样式而在写入路径的第一行。触摸、撤销、清空和确认都先判断viewModeif (this.viewMode preview) { this.signatureStatus 历史签名只读请先退出预览后新建签名; return; }这段判断能够证明当前页面不会在预览模式继续修改strokes或新增本地历史记录。它不能证明用户在其他页面、其他进程或真实工单系统中也没有修改历史数据这个组件没有跨页面锁定和服务端权限校验的证据。把边界写清楚比把“笔迹锁定”夸大成全面的业务锁定更可靠。退出预览和重新开始的语义也不相同。exitPreview()只清除选中历史项、回到新建模式让用户继续处理已有的strokesstartNewSignature()在此基础上还清空strokes创建一张新的本地签名画布。两者都要重绘否则状态文案虽然已变Canvas 仍可能保留历史笔迹形成“可写模式却看着像历史记录”的误导。撤销与清空先改变来源数据再让画布跟随维修签名常见的两个操作是撤销最后一笔和清空整张签名板。它们不是对 Canvas 像素做局部覆盖而是先更新strokes再调用redraw()private undoStroke(): void { if (this.strokes.length 0) { this.signatureStatus 没有可撤销的笔画; return; } this.strokes this.strokes.slice(0, this.strokes.length - 1); this.activeStroke undefined; this.signatureStatus this.strokes.length 0 ? 已撤销全部笔画 : 已撤销最后一笔; this.redraw(); } private clearSignature(): void { this.strokes []; this.activeStroke undefined; this.signatureStatus 签名画布已清空; this.redraw(); }这条顺序有明确的因果关系。slice()返回去掉最后一项后的新数组下一次重绘就不会再把最后一笔画出来清空数组后重绘仍会保留背景和基线但不会有任何drawStrokes()的笔迹输入。若反过来先擦画布、后更新数组下一次触摸、尺寸变化或抗锯齿切换触发重绘时被“擦掉”的旧笔迹还会从数组中重新出现。同样需要避免把状态文案当结果证据。“已撤销最后一笔”只能说明当前组件完成了本地数组更新“签名画布已清空”不等于本地历史记录被删除更不等于真实工单中的任何签名被撤回。页面没有实现远程删除或历史项删除逻辑所以正文不能把这两个按钮说成工单操作。有笔迹不一定能确认页面如何识别过短输入仅凭strokes.length 0判定签名完成会把一次误触也计为签字。当前页面用isValidStroke()过滤过短笔画少于三个点直接无效满足点数后再累加相邻点之间的二维距离只有总长度不少于 12 才算有效。private isValidStroke(stroke: Stroke): boolean { if (stroke.points.length 3) { return false; } let length 0; for (let index 1; index stroke.points.length; index 1) { const horizontal stroke.points[index].x - stroke.points[index - 1].x; const vertical stroke.points[index].y - stroke.points[index - 1].y; length Math.sqrt(horizontal * horizontal vertical * vertical); } return length 12; }这段实现能确认的是一个很具体的页面准入条件当前坐标样本至少包含三个点且累计轨迹长度达到 12。它不能验证签字人的身份、签名字形是否符合公司制度、笔迹是否可作为法律凭据也没有给出任何防伪、时间戳签名或服务端验签逻辑。它的作用是让“请先完成有效签字”不只是空提示而是有一个能从当前源代码核对的本地阈值。对读者而言最值得复核的是操作路径而不是猜一个签名看起来是否像真的先只短按或短划一次确认状态仍提示“笔迹过短”再完成一条多点且足够长的笔画确认状态变为“笔迹已采集可确认本地归档”。这能验证当前页面的本地阈值分支没有业务系统回执时不能把它称为验收通过或工单完结。点击确认后为什么我仍只写“本地确认”confirmSignature()先禁止预览态确认再检查有效笔画数。满足条件时它将当前笔迹坐标归一化然后追加一条历史记录private confirmSignature(): void { if (this.viewMode preview) { this.signatureStatus 历史签名只读请先退出预览后新建签名; return; } const count this.validStrokeCount(); if (count 0) { this.signatureStatus 请先完成有效签字; return; } const record: SignatureHistoryRecord { id: local-${this.signatureHistory.length 1}, action: 完工确认, signer: 维修人员, signedAt: 刚刚, strokes: this.normalizedStrokes() }; this.signatureHistory [record, ...this.signatureHistory]; this.signatureStatus 本地确认已记录共 ${count} 笔有效笔画未回写真实工单; this.redraw(); }关键事实就在状态文案最后七个字未回写真实工单。这里的确认结果是页面内存中的signatureHistory新增一项方便在左侧历史列表中预览源代码没有网络请求、数据库写入、工单接口回执或失败重试逻辑。因此无论按钮写着“确认并归档”还是历史列表数量增加都只能描述为本地演示记录。normalizedStrokes()的存在也有实际原因。新建签名使用当前 Canvas 的像素坐标存入历史项前页面按当前宽高换算为 240×130 基准坐标。后续历史预览再按照当前画布尺寸放大能够减少“在一个尺寸写入、换一个尺寸预览时位置完全失真”的问题。它能证明页面进行了本地坐标归一化不能证明记录离开当前页面后仍然完整可用因为没有持久化和跨设备同步的实现。场景解说此图应显示有效笔迹后的确认操作、历史列表新增记录和“未回写真实工单”提示只能证明本地历史状态变化不能代替外部工单系统的回执证据。抗锯齿是重绘条件不是另一篇文章的主线签名页提供antialiasEnabled和toggleAntialias()但这里它服务于手写笔迹的显示状态而不是把整篇写成文字边缘对比。切换时先尝试写入当前绘制上下文再无论成功或回退都调用redraw()private toggleAntialias(): void { const previous this.antialiasEnabled; this.antialiasEnabled !previous; if (this.applyAntialias()) { this.antialiasStatus this.antialiasEnabled ? 抗锯齿已开启 : 抗锯齿已关闭; } else { this.antialiasEnabled previous; this.applyAntialias(); } this.redraw(); }这条链路能确认页面将本地开关尝试应用到CanvasRenderingContext2D若当前运行环境抛出异常就恢复之前的开关值随后再重画签名板。它解决的是“按钮文本变了但画布仍保留旧像素”的一致性问题。我不会把它描述成“关闭后签名一定更锯齿”或“开启后每台设备都更平滑”。源码只处理设置与重绘没有提供真机截图、像素测量或设备对照。正确的观察顺序是记录切换前状态点击开关确认状态文案与按钮一致再观察签名字迹是否随完整重绘更新若需要评价视觉差异必须补充相同画布尺寸、相同笔迹和目标设备上的实际取证。我按这条操作链做人工复核为了把“页面显示了签名”与“业务完成”分开我会按以下顺序核对当前工程页面进入FullScreenSignaturePage确认新建模式提示为“请在签名板内书写”签名板为空且带有基线。在签名板内完成一条连续笔迹观察状态从“正在采集现场签名…”变为有效笔迹提示短划或短按时应保留“笔迹过短”分支。点击“撤销”确认最后一笔从 Canvas 消失点击“清空画布”确认当前新建笔迹清除但页面基线仍保留。选中一条历史记录确认页面进入历史预览并拒绝继续书写、撤销、清空和确认退出预览后确认可重新进入新建签名流程。写入有效笔迹后点击“确认并归档”确认本地历史列表增加记录并且状态明确显示“未回写真实工单”。切换抗锯齿确认按钮和状态文案同步仅在补充同尺寸目标设备取证后才对边缘差异做视觉结论。这六步覆盖当前页面可观察的本地状态变化。它们不包含账号、服务端工单、数据库或真实签字人验证因为工程文件里没有对应调用链。人工审核时若需要发布“工单已更新”“记录已归档到后台”之类的结论应先补充外部系统回执、接口路径和失败处理证据不能从这张 Canvas 的页面状态推导。结论重绘负责对齐本地状态不负责替业务背书维修签名页的难点不在于画出一条线而在于让每一次本地状态变化都有唯一、可追溯的画面结果。触摸采集更新的是当前笔画预览切换更换的是可见笔迹来源撤销和清空先改变数组再重画确认新增的是本地历史记录抗锯齿切换改变的是绘制配置。它们都需要通过redraw()回到同一条绘制路径才能避免旧像素留在画布上误导操作人。与此同时重绘的责任到 Canvas 为止。它能让读者观察到本地状态与签名板一致不能让页面凭空拥有真实工单回写、服务端持久化、身份认证或跨设备同步能力。把这一层边界说清楚才是我在维修签名页中坚持每次状态变化后重新绘制的原因。必要条件运行条件矩阵必要条件工程位置运行前动作未满足时现象SDK/API与构建工具build-profile.json5、Hvigor确认 compile/compatible/target 均为 API 24类型检查或构建失败Kit引入当前文章对应页面 import检查 Kit 名称和 API 是否与 API 24 匹配编译期找不到类型或方法模块与页面配置module.json5、main_pages.json确认页面已注册、模块为 Stage页面无法启动或路由失败设备权限与动态授权requestPermissions、abilityAccessCtrl先查询并申请权限能力初始化被拒绝或跳过系统能力与硬件设备能力、Camera、MapKit、VisionKit等在目标设备检查能力进入不支持、等待或人工降级状态矩阵中的 SDK/API 行核对完成后插入构建证据需同时看到 API 24 和构建工具信息设备权限行核对完成后插入授权证据系统能力与硬件行核对完成后插入版本/能力证据MapKit文章在系统能力行后增加AppGallery Connect 项目已创建或选定应用包名和签名证书与工程一致MapKit 服务已开通并按控制台要求完成应用服务凭据/授权配置。截图不得带出密钥或证书私钥。