在这里插入图片描述

我有一次把 Canvas 的 AA 开关点来点去,最后留下两张截图:一张按钮写着“AA 开”,另一张写着“AA 关”。缩小放到聊天窗口里一看,两张图像是同一张。我当时的第一反应很直接:开关没生效。

这个判断后来被我自己推翻了。不是因为我突然从图里看出了多大的差别,而是我把“截图看着一样”和“绘制参数没有生效”这两件事拆开了。前者只是一次观察,后者是对调用和输出的判断。缩小图把文字、斜线、边框都压成很少的像素,边缘上本来就细小的过渡很容易一起被吞掉。它不能单独承担结论。

这篇只说一个故障线:我把缩略截图当成了开关结果,差点在错误方向上查了很久。页面里已经有固定输入、上次快照、边缘放大区和 redrawStatus,但我起初只盯着最不可靠的那一层,等于把几个能帮我排除错觉的观察点全浪费了。

我不打算把这件事写成“开了 AA 一定怎样、关了 AA 一定怎样”的视觉承诺。不同设备、字体和截图缩放方式会影响肉眼感受。这里能从现有页面确认的是:一次切换前后的状态有没有被保留、当前绘制有没有重新发生、页面准备的放大提示是否和当前参数一致。真实边缘像素还要在同尺寸真机上复核。

现象很普通,误判也很普通

页面初始输入是 工单 WO-6101、84px、900 字重,AA 默认开启。这个组合不是随便摆在页面上的演示内容,它给比较留了一个固定起点。只要我不去点“切换样本”、字号和字重按钮,那么前后变化的候选项就只有 antialiasEnabled

可我第一次操作没有守住这个前提。我点 AA 后截一张图,又为了让字更清楚点了一下字号,再把图片缩小发给同事。结果当然很难解释:字形、位置和边缘信息同时被改动,还经过了缩放。那两张图即使看起来相同,也说明不了 AA;即使看起来不同,也说不清差异来自哪个变量。

我后来把动作收成一条短规则:比较 AA 前后时,文本、字号、字重、画布尺寸和截图倍率都不动。不是为了把测试写得很隆重,而是为了让“看不出来”这句话有边界。输入没固定之前,任何视觉判断都像在比较两道不同题目的答案。

先看页面如何把这些输入打包成一次快照:

private currentSnapshot(): SnapshotState {
  return {
    sampleText: this.sampleText,
    fontSizeValue: this.fontSizeValue,
    fontWeightLabel: this.fontWeightLabel,
    antialiasEnabled: this.antialiasEnabled
  };
}

private capturePreviousSnapshot(): void {
  this.previousSnapshot = this.currentSnapshot();
  this.hasPreviousSnapshot = true;
}

这段代码让我改掉了一个不好的比较习惯。以前我会凭记忆说“刚才好像更平滑”。现在页面在改变参数前先调用 capturePreviousSnapshot(),把当时的文本、字号、字重和开关值留住。右侧不是“我印象里的上一张”,而是被保存下来的上一组输入。它仍然不是对屏幕物理像素的检测器,但至少把对照条件钉住了。

我还特别检查了捕获时机。它必须在反转 AA 状态之前发生。要是先改状态、后抓快照,当前输出和上次快照会带着同一个 antialiasEnabled,页面虽然出现了两个面板,却没有前后对照的意义。那种界面很容易让人误以为功能完整,因为看起来“有历史记录”,实际保存的是重复数据。
在这里插入图片描述

我先查了什么,又为什么不够

看到缩略图相同后,我先做的是看按钮和顶部文字。按钮会在 AA 开AA 关之间变化,顶部状态也会写出从开启到关闭或反过来的切换。这个结果只能说明点击回调和页面状态走通,不能说明我看到的缩略图有足够分辨率。

第二步我检查底部的 redrawStatus。它是在 renderCanvas() 结束时由当前快照写入的:

const current = this.currentSnapshot();

this.drawSnapshotPanel(current, margin, safeTop, panelWidth, topPanelHeight, '当前输出', '#B54708');
if (this.hasPreviousSnapshot) {
  this.drawSnapshotPanel(this.previousSnapshot, margin + panelWidth + gap, safeTop, panelWidth, topPanelHeight, '上次快照', '#1D4ED8');
}
this.drawMagnifier(current, margin, lowerY, this.canvasWidth - margin * 2, lowerHeight);
this.redrawStatus = `已渲染 ${current.sampleText} · antialias=${current.antialiasEnabled ? 'true' : 'false'}`;

这比只看按钮多了一层确认:当前快照已经被送进绘制函数,页面完成了一次绘制,并把本次参数写到了状态区。假如按钮变了而 redrawStatus 还停在旧的 antialias=true,我就该先查重绘路径,而不是去放大截图找边缘。反过来,redrawStatus 已变成 false,也不能让我直接宣称任何设备上都会有肉眼可见差异;它只把排查范围从“有没有重绘”缩到“怎样观察重绘结果”。

当时我就是少做了这一步。缩略图看上去没变化,脑子里已经默认了“开关失败”,却没有先看页面有没有明确表示使用新快照渲染。这个顺序一倒,后面看到什么都会服务于先入为主的结论。

故障不是 AA 本身,而是观察顺序断了

把当时的错误过程画出来,就能看出我在哪里跳步了。点击与状态更新实际都可能正常,而我把经过缩放的图片直接拿去判断底层参数。

固定页面初始状态

只点击一次 AA 按钮

antialiasEnabled 改变

保存前次快照并重绘

页面显示当前输出和上次快照

截图被缩小或二次压缩

细小边缘信息减少

错误判断: 两张图相同就是开关没生效

先核对 redrawStatus 与快照字段

再查看边缘放大区

把页面可确认与真机像素观察分开

这张图里最重要的不是“截图压缩会丢信息”这个常识,而是 I 这一步。状态文字、快照和放大区不是让页面显得更丰富的装饰,它们各自回答不同问题:当前输出是否使用了新参数,上次输入有没有保存,页面是否提供了针对边缘的观察入口。把三者混成一张缩略图,等于主动丢掉定位信息。
在这里插入图片描述

放大区能帮我什么,不能替我证明什么

页面的 drawMagnifier 会依据快照中的开关值选择两组色板,并绘制“平滑过渡”或“硬边界”字样和一块网格。它的价值是把本次页面状态放在一个不需要眯眼的区域里,方便我确认比较目标没有跑偏。

const smoothPalette = ['#FFFFFF', '#FDE68A', '#F59E0B', '#7C2D12'];
const hardPalette = ['#FFFFFF', '#FFFFFF', '#111827', '#111827'];
const palette = snapshot.antialiasEnabled ? smoothPalette : hardPalette;
this.applyAntialias(ctx, snapshot.antialiasEnabled);
ctx.fillText(snapshot.antialiasEnabled ? '边缘放大:平滑过渡' : '边缘放大:硬边界', x + 28, y + 38);

这里我给自己留了一个分寸:这块网格是页面根据状态画出的放大提示,不是从文字边缘截取出来的一块逐像素证据。它证明的是页面正在按哪种观察语义展示,帮助测试时避免用缩略图猜;它不能代替真机截图,也不能证明某款设备的字体栅格化结果一定如此。

不过,这并不让放大区失去作用。排查里最怕的不是工具不万能,而是不知道工具回答的是什么问题。放大区回答“我现在应该看哪类边缘特征”,redrawStatus 回答“当前绘制用的开关值是什么”,上次快照回答“我的对照对象有没有被固定”。把这三个回答拼在一起,就比“这张图看起来差不多”可靠得多。

我会把当前输出和上次快照并排观察。两块面板都会显示 antialias=true/false、字号和字重。只有两块的文本、字号、字重相同,而开关值不同,才进入下一步看斜线、文字和边框的边缘。如果标签上显示连输入都不一样,我会立即重置页面再做,不在一组混杂变量的图片上继续分析。
在这里插入图片描述

回到代码后,我确认的调用顺序

AA 按钮实际走的是先保存旧值、反转状态、尝试写入上下文、再重绘。写入不被运行环境接受时,页面会恢复原有开关,再调用一次渲染。这里的回退很关键,它避免状态文字说“关”,但画布仍按旧配置画的自相矛盾情况。

观察区域 Canvas上下文 页面快照 用户 观察区域 Canvas上下文 页面快照 用户 alt [设置可用] [设置不可用] 点击 AA 开关 保存 previousSnapshot 反转 antialiasEnabled applyAntialias(新值) true 绘制当前输出、上次快照、放大区 更新 redrawStatus false 恢复旧状态 按恢复后的值重绘并显示状态

我不会把 applyAntialias 成功与“图片一定不同”画上等号。前者说明赋值路径没有在页面这一层报错,后者还受显示和观察条件影响。但如果回退分支发生,顶部会提示当前运行环境不支持动态抗锯齿,这时我不会再拿截图做比较,而是先记录环境限制。否则“效果不明显”和“能力不能动态切换”会被混成一个问题。

另一个容易漏掉的前置条件是 canvasReadyrenderCanvas() 会在 Canvas 未就绪时直接返回,而 Canvas 的 onReady 与区域变化回调会再触发渲染。所以我也会先确认底部状态不是初始化期间的旧文本。页面未准备好时讨论截图,没有意义;那时画布还没进入可供比较的稳定状态。

我的复测动作固定成这样

我不再一边随手点按钮一边截屏。先进入页面,确认固定文本仍是 工单 WO-6101、84px、900 字重,且第一次右侧显示的是等待产生对照的空面板。然后我只点一次 AA,不动样本、字号、字重,也不旋转设备。

点完后先不看整张画布,按顺序看四处:按钮标签是否翻转;顶部状态是否记录本次开关;右侧是否出现上次快照且仍标注旧值;底部 redrawStatus 是否与当前开关一致。四处都对,说明页面里能观察到的状态和渲染调用没有互相打架。随后我才查看下方放大区,再决定是否需要保留同尺寸的真机截图。

下面是我实际会交给测试同学的流程。它并不要求对视觉作夸张判断,每个停止点都对应一个可回到代码的方向。

进入页面

Canvas 已准备好

等待 onReady 或检查初始化

确认固定文本、84px、900字重

记录初始 redrawStatus

只点击一次 AA

当前按钮和顶部状态已切换

检查点击回调和状态写入

右侧是否保留旧快照

检查 capturePreviousSnapshot 时机

redrawStatus 是否写入当前值

检查 renderCanvas 是否完成

查看放大区的当前状态提示

同尺寸真机截图复核实际边缘

分别记录页面状态与视觉观察

这里有一个我现在很在意的细节:页面的按钮“切换样本”、字号、字重也是可用操作,但 AA 对照期间不能顺手碰。它们都会在改变前捕获快照,然后触发重绘。功能本身没有问题,问题在于我若同时操作多个按钮,右侧快照保存的就是另一轮组合,后续再从缩略图里追原因几乎不可能。
在这里插入图片描述

我还给截图本身加了一个很简单的约束:比较图保留原始尺寸,或者至少把两张图按同一个倍率显示。很多“完全一样”的结论,是在应用预览、即时通讯缩略图或文档排版中得出的。这些工具会重新采样,文字的斜边和一两像素宽的过渡最先被合并。即使两次绘制的局部不同,经过一次统一缩小,也可能变成同一片颜色。把缩略图当作最终裁判,相当于先把要观察的东西抹掉,再质疑它为什么不存在。

为了避免记忆干扰,我不会连续点五六次再回头挑两张图比较。每一轮都从一个已知起点开始:先重开页面或明确记录当前 antialias 值,保持固定输入,只切一次,然后立即记录当前面板和上次面板的字段。下一轮若要再切回去,也要把上一轮结果当作新的前次快照看待。这样得到的是一串相邻状态的比较,而不是从许多混杂截图里挑两张看上去接近的图。

还有个反例值得写下来:假如当前面板标签是 antialias=false,上次快照标签也是 antialias=false,下方却写着“硬边界”,我不会因为画面提示符合预期就放行。这更像是快照捕获太晚,或者前一轮输入本来就处于关闭状态。此时问题不在截图缩放,而在对照组失效。只有当前与上次的 AA 字段不同,截图相同才值得被解释为观察分辨率不足,而不是简单的数据重复。

我也不会因为面板标签一真一假、放大区也随之改变,就跳到“真实文字一定有明显差异”。当前页面的放大区是为观察建立的提示区,它帮助我把注意力落到边缘,不承担各设备字体渲染结果的承诺。真正准备对外展示时,我会附上设备型号、系统版本、画布尺寸、是否经过二次缩放这些条件;条件缺失的图片,只能说明这次页面状态是什么,不能用于比较画质高低。

记录时我会把操作顺序也写进去:从哪个开关值开始、点了几次、截图取自当前输出还是上次快照。这样隔几天回看,图片不会只剩“像”和“不像”两种印象。

这次留下的不是截图技巧

这次踩坑让我不再用“看起来没变”直接否定一个 Canvas 开关。它不是让我盲目信任状态标签,而是让我先问清楚:我手里的图片是否适合观察这种差异;当前输入是否固定;前一轮状态是否真的保存;本轮是否走完渲染;页面放大的到底是观察提示还是实际像素。

对这个 Demo 而言,页面内可以确认的是:固定输入下,切换动作会留下旧快照,当前输出、放大区提示和 redrawStatus 都以当前状态组织。仍需外部补看的,是同一设备、同一尺寸、同一截图倍率下,文字和斜线边缘是否有可见差异。把这两类结果写在一起,结论就会过头。

以后再遇到两张缩略图“几乎一模一样”,我会先停止放大猜测,回到这些状态点。截图相同只是提醒我观察条件可能不够,不是对功能的判决。这个顺序看上去慢一点,实际能省掉大量围着一张小图来回争论的时间。

必要条件|运行条件矩阵

必要条件 工程位置 运行前动作 未满足时现象
SDK/API与构建工具 build-profile.json5、Hvigor 确认 compile/compatible/target 均为 API 24 类型检查或构建失败
Kit引入 当前文章对应页面 import 检查 Kit 名称和 API 是否与 API 24 匹配 编译期找不到类型或方法
模块与页面配置 module.json5main_pages.json 确认页面已注册、模块为 Stage 页面无法启动或路由失败
设备权限与动态授权 requestPermissionsabilityAccessCtrl 先查询并申请权限 能力初始化被拒绝或跳过
系统能力与硬件 设备能力、Camera、MapKit、VisionKit等 在目标设备检查能力 进入不支持、等待或人工降级状态

矩阵中的 SDK/API 行核对完成后插入构建证据(需同时看到 API 24 和构建工具信息):

在这里插入图片描述

设备权限行核对完成后插入授权证据:

在这里插入图片描述

系统能力与硬件行核对完成后插入版本/能力证据:

在这里插入图片描述

MapKit文章在系统能力行后增加:AppGallery Connect 项目已创建或选定,应用包名和签名证书与工程一致,MapKit 服务已开通并按控制台要求完成应用服务凭据/授权配置。截图不得带出密钥或证书私钥。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐