HarmonyOS6.1.1-Canvas:维修签名-开关变化显式重绘

发布时间:2026/8/14 13:05:40
HarmonyOS6.1.1-Canvas:维修签名-开关变化显式重绘 我第一次给 Canvas 加抗锯齿开关时以为这就是一行赋值的事。this.antialiasEnabled !this.antialiasEnabled;点击按钮后页面文字已经从“AA 开”变成了“AA 关”。看上去没有异常。可我盯着画布里的文字和斜线看了半天画面像是从头到尾都没有变。最初我把问题归到设备渲染差异上。后来才发现问题比“这个设备看不出差别”更基础我只改了页面状态没确保状态变化重新进入 Canvas 的绘制流程。那次排查的过程很像在找一盏明明亮着开关、灯泡却不亮的灯。按钮文案是开关antialiasEnabled是墙里的线路Canvas 里的像素才是灯泡。开关拨下去并不说明最后一环已经通电。这个比喻听起来朴素但在 UI 开发里特别有用因为我们每天都在看按钮、标签、Toast 这些“已经变化”的东西很容易因此相信整件事已经完成。下面这篇不是讲 Canvas 有哪些接口也不是做性能横评。我只复盘这一个场景一个 AA 开关看上去已经切换为什么画布仍然像没收到通知一样停在旧画面里。在项目里这种问题往往比崩溃更磨人。崩溃至少会给你一个明确的信号这里出事了。画布不重绘则更安静页面没有报错按钮也还活着甚至日志里也未必有一行红字。它让人产生一种错觉仿佛功能已经完成只是“效果不太明显”。如果这个错觉带进业务页面后面接手的人会反复改字号、换字体、调颜色最后在完全错误的方向上花掉半天。所以我给自己定了一个规矩凡是命令式绘制的交互不先讨论画面好不好看先确认它有没有重新执行绘制命令。先确认“有没有发生”再讨论“发生得好不好”。故障现场按钮变了画布没动这个 Demo 固定用工单 WO-6101、84px、900 字重做样本。固定输入不是为了做一个漂亮的展示而是避免排查时又换文字、又换字号、又换字重最后根本不知道是什么导致了画面变化。页面上的 AA 按钮由antialiasEnabled控制。假如点击逻辑只有下面这样private toggleAntialias(): void { this.antialiasEnabled !this.antialiasEnabled; this.antialiasStatus this.antialiasEnabled ? 抗锯齿开启 : 抗锯齿关闭; }按钮文案肯定能更新因为它依赖 ArkUI 的状态。但CanvasRenderingContext2D不会因为一个普通状态字段变化就自己重新执行绘制命令。上一次fillText、stroke和fillRect产生的像素还留在画布上所以用户看到的就是“开关好像失灵了”。这里容易被页面状态骗到。UI 状态更新只能证明 UI 状态更新它不等于 Canvas 已经使用了新配置重画。当时我是怎么一步步查偏的一开始我先怀疑按钮没有进点击事件。这个判断很好验证连续点两次按钮能在AA 开和AA 关之间来回切换顶部状态也会变。说明事件进来了State也更新了。第一条怀疑可以划掉。接着我怀疑是抗锯齿差异太细肉眼在当前缩放比例下看不出来。这个怀疑也很合理。抗锯齿不是换颜色不会让整个页面突然“变脸”它更多影响文字、斜线和边框交界处的过渡。所以我把注意力放到了页面里的放大区固定了文字、字号和字重避免每一次点按钮时同时改变其他变量。可问题仍然在放大区也没有新的变化。到这一步再继续讨论“效果是否明显”已经没有意义了。因为我们甚至还没确认新设置有没有重新参与绘制。于是排查顺序改成了更笨、但更可靠的方式不再问“为什么看起来没差别”先问“这次点击之后到底有没有重新画”。这两个问题很像答案却可能完全不同。前一个容易把人带去设备、屏幕、字体渲染后一个会把人带回最基本的调用路径。把故障过程摊开看在没有重绘调用的旧写法里流程会在状态更新后断掉。按钮、顶部文本都依赖状态因此它们先变化Canvas 不是自动响应式的它仍然保留上次绘制的像素。否是点击 AA 开关antialiasEnabled 状态取反按钮文案更新为 AA 开或 AA 关是否调用 renderCanvasCanvas 保留上一次像素现象: 文案变了 画布没变清空旧画布读取 currentSnapshot按最新 antialias 设置绘制更新 redrawStatus 和放大区这张图也是这篇文章的主线。它没有把问题说成“AA API 无效”因为现有代码里真正需要保证的是状态更新之后调用链是否抵达renderCanvas()。一旦把问题定义准确修复通常不复杂困难的部分只是别让按钮的变化提前替我们下了结论。我先检查了三个地方第一个是按钮文本。它会变因此这不是点击事件失效。第二个是ctx.antialias是否真的被设置。现有页面把它封装成applyAntialias原因并不复杂这个赋值可能因为运行环境不支持而抛出异常。若直接往下执行就会出现另一种更难排的假象状态写成“已关闭”实际绘制上下文仍保持旧设置。private applyAntialias(ctx: CanvasRenderingContext2D, enabled: boolean): boolean { try { ctx.antialias enabled; return true; } catch (_error) { this.antialiasStatus 当前运行环境不支持动态抗锯齿; return false; } }第三个才是关键设置完成后有没有执行renderCanvas()。它负责清空旧画布、读取当前快照、重新画出当前输出、上次快照和边缘放大区。如果这一步没有发生前两步即使都正确用户仍然只能看到旧像素。我在这里才意识到Canvas 与普通声明式组件的思维方式不一样。普通组件里状态变了框架会负责把依赖这个状态的 Text、Button 再次计算出来。Canvas 更像一张白板你上一次画上去的线不会因为旁边变量换了值就自动消失。要先擦再按新参数重画。没有这一步旧画面会非常诚实地留在那里只是它诚实地暴露了我们的遗漏。实际的renderCanvas()并不只是把文字再写一遍。它会先clearRect清掉旧帧接着画当前输出如果存在上一轮快照就画第二块对比面板最后画边缘放大区。于是页面里的三个视觉区域不是三张互不相干的图而是同一次状态快照的三个视角。也正因为这样快照捕获放错时机时故障会变得很隐蔽画面确实重画了但“当前输出”和“上次快照”取到的是同一个状态用户看到两块几乎相同的内容很自然会误判开关没有作用。这也是我没有只写“调用renderCanvas()就好了”的原因。修复不是往函数尾部随手补一行而是先弄清这一次点击前后哪些数据应该属于旧状态哪些应该属于新状态。Canvas 的问题经常藏在这种先后顺序里。修复不是多加一行而是确定调用顺序最后保留的实现顺序是先保存旧快照再修改状态再向绘制上下文应用设置最后重绘。若上下文拒绝动态设置则回退到修改前的状态再重绘一次以免按钮和画面各说各话。private toggleAntialias(): void { const previous this.antialiasEnabled; this.capturePreviousSnapshot(); this.antialiasEnabled !previous; if (this.applyAntialias(this.context, this.antialiasEnabled)) { this.antialiasStatus 已从${previous ? 开启 : 关闭}切换为${this.antialiasEnabled ? 开启 : 关闭}; } else { this.antialiasEnabled previous; this.applyAntialias(this.context, previous); } this.renderCanvas(); }这段逻辑有两个容易被忽略的点。一是快照必须在状态反转之前保存。否则“上次快照”和“当前输出”会拿到同一份数据右侧对比区看起来存在实际没有比较价值。二是重绘必须放在成功和失败分支之后。这样不支持动态抗锯齿的环境也会刷新状态区用户能看到“当前运行环境不支持动态抗锯齿”而不会停留在某个旧画面上猜发生了什么。还有一个小细节我后来才重视renderCanvas()的开头会检查canvasReady。这意味着页面刚创建、Canvas 还没有 ready 时就算状态先变了也不能硬画。这个保护避免了拿着未就绪的绘制上下文去调用一串 API。等onReady或尺寸变化回调到来页面再走一次renderCanvas()此时使用的是最新状态。private renderCanvas(): void { if (!this.canvasReady) { return; } // 清空旧像素后按 currentSnapshot() 的最新状态重新绘制。 }这也是为什么排查这类问题时不能只在点击函数里找答案。一次“点开关没效果”至少横跨三个时刻页面是否准备好、点击时状态是否变更、状态变更后是否重新绘制。把它们揉成一个“开关失败”日志和代码都会变得很难读。修复后的调用链修复后我会用下面这条链路审阅每一次参数切换。它把正常路径和不支持动态设置的回退路径分开避免页面表面“成功”内部却保留旧配置。renderCanvasCanvas 上下文页面状态用户renderCanvasCanvas 上下文页面状态用户alt[设置成功][设置失败或环境不支持]点击 AA 开关capturePreviousSnapshot()antialiasEnabled 取反applyAntialias(enabled)true更新 antialiasStatusrenderCanvas()清空并重画当前快照、历史快照、放大区更新 redrawStatusfalse恢复 previous 状态applyAntialias(previous)renderCanvas()展示回退后的可解释状态这里的回退并不是为了把异常藏起来。相反它是为了避免两个事实冲突页面说“已经关闭抗锯齿”Canvas 却还在按“开启抗锯齿”的配置绘制。恢复旧状态后当前结果至少是一致的再配合当前运行环境不支持动态抗锯齿这条状态使用者知道下一步该检查环境而不是继续点击按钮碰运气。我怎么确认这次真的走到了重绘不需要靠肉眼盯着一行文字猜。页面在renderCanvas()末尾会更新redrawStatusthis.redrawStatus 已渲染 ${current.sampleText} · antialias${current.antialiasEnabled ? true : false};所以这次检查可以拆成三步点击AA 开或AA 关。看顶部状态是否记录了从开启到关闭或从关闭到开启的切换。看底部是否出现与当前状态一致的已渲染 ... antialiastrue/false并确认上次快照已出现。前三项都成立说明当前页面已经完成“状态变更 - 上下文配置 - 重绘请求”的本地链路。至于不同设备上边缘究竟有多明显仍需要在相同尺寸的真机上看放大区域。不能因为 Demo 有一块“平滑过渡/硬边界”的说明区就把它写成所有机型的实测视觉结论。一次复测应该像走一条短路线我现在复测这类交互会把动作压缩成一条短路线不靠“多点几次看看”。先打开页面确认初始样本仍是工单 WO-6101、84px、900 字重按钮显示AA 开。接着只点一次 AA 按钮不切换样本、不改字号、不改字重。此时应该先出现上次快照再看到当前状态改为关闭底部渲染状态也变成antialiasfalse。最后把视线放到边缘放大区而不是试图从整页截图里凭感觉找细微差异。如果其中某一步不对定位会很快按钮不变先查点击事件按钮变、底部渲染状态不变查有没有调用renderCanvas()底部状态变、但上次快照没有出现查快照保存时机状态和快照都对、视觉仍无差异再去核对当前设备与真机环境。这样每一步都把问题往更小的范围里收不会一开始就把锅甩给“系统渲染不稳定”。测试流程不要从“看起来差不多”开始这类页面最容易写出一句无效用例“点击开关观察画布是否变化。”它的问题是没有固定输入也没有规定观察点。测试的人只能凭感觉判断得到的结果很难复现。我更推荐把测试走成下面的流程。每个菱形都是一个停止点没有通过时不要继续往后猜而是回到对应的代码或环境位置补证。否是否是否是否是否是进入 Canvas Anti-Alias LabCanvas 是否 ready等待 onReady 或记录页面初始化异常确认固定输入: 工单 WO-6101 84px 900记录初始 AA 状态与 redrawStatus只点击一次 AA 开关按钮与顶部状态是否切换检查 onClick 与 antialiasEnabled 状态写入底部 redrawStatus 是否更新检查 toggleAntialias 是否调用 renderCanvas上次快照是否出现且保持旧配置检查 capturePreviousSnapshot 的调用时机检查边缘放大区和当前 antialias 值是否需要真机视觉结论记录本地调用链验证完成同尺寸真机截图对比并记录设备环境这张测试图的价值不在于把一次点击搞得很复杂而是防止“看到一个现象就跳到一个结论”。例如redrawStatus没有更新时测试到这里就应停止不能继续评价放大区的像素差异因为它们都还是上一次绘制留下来的内容。反过来redrawStatus已更新但真机上看不出差异也不应回头说“重绘没有发生”那是另一个问题需要记录设备、分辨率和截图倍率再比较。写给接手这个页面的人如果以后需要在这个页面继续加能力我建议不要把所有变化都塞进 AA 开关里。比如新增画笔粗细、文本颜色、背景主题或缩放比例时每个交互都应沿用相同的骨架先抓取旧快照再改变一个参数再在当前环境中应用配置最后统一重绘。这样做的好处是所有参数切换都有一致的排查入口测试人员也能复用上面的流程图。更重要的是别为了让演示看起来热闹同时切换多个变量。一次点击如果既换样本、又改字号、又换 AA 状态任何视觉变化都无法归因。当前 Demo 把“切换样本”“字号”“字重”和“AA”放成独立按钮正是为了让每一步可以单独复测。单个操作显得慢一点问题发生时却快得多。有时候工程里的文采并不在于把技术写得多么宏大而在于把一个原本模糊的故障讲清楚哪一步看似已经完成哪一步实际还没有发生最后怎样让两者重新对齐。这个 Canvas 开关的坑很小但它提醒我用户眼里的一次点击背后往往不是一件事而是一串需要逐一兑现的承诺。这不是一个只属于 AA 的问题这个坑以后还会在别的 Canvas 场景里出现。比如改画笔宽度、主题色、缩放级别、图片滤镜甚至只是切换一段绘制文本。只要底层是命令式绘制都会面对同样的分界线状态有没有更新是一回事状态有没有被重新消费并画到屏幕上是另一回事。我更愿意把这次问题记成一句很短的话状态不是画面重绘才是画面。它不华丽但在排查时很管用。以后看到“按钮已经变了内容却没变”先别急着怀疑 API先顺着这句话检查一遍。这次踩坑留下的三个检查项改 Canvas 相关状态后是否明确调用了渲染函数而不是只更新文字配置写入失败时页面状态是否回退避免显示一个并未生效的值对比数据是否在改变参数之前保存保证“前后”确实来自两次不同状态我现在会把 Canvas 的交互看成两条链路一条是 ArkUI 状态链负责按钮和文案另一条是绘制命令链负责真实像素。两条链路必须在一次操作里同时走完。只盯着前者最容易得到“功能看上去完成了画布其实没更新”的假成功。本文只讨论现有 Demo 中的状态、调用和重绘路径。不同设备的实际边缘效果仍应结合 API 24 运行环境和同尺寸真机截图复核。