HarmonyOS 7 ArkUI:无障碍语义漂移与触控目标门禁
一、审核失败不是“少写了一个标签”这次的起点是一份很短的测试反馈读屏进入订单列表后三个“更多”按钮读出来完全一样把订单改成已退款按钮视觉已经变成“查看退款”读屏却仍然播报“申请退款”右上角筛选图标的触控区域只有 32 vp。单看任何一项都不难修麻烦在于它们都不是稳定复现的静态缺陷而是随着列表复用、状态切换和弹窗关闭不断漂移。项目叫 A11yReleaseGate页面是 AccessibilityAuditPage。我们没有再靠人工逐页点而是做了一个上架前门禁任务 A11Y-1712扫描 24 个页面、486 个可交互节点把无标签、重复标签、小触控目标和焦点环路收敛为一份可追踪报告。第一次跑出来的数据不算好看无标签 7 个、重复标签 3 组、小触控目标 5 个、焦点环路 1 条。真正改变处理方式的判断是无障碍信息不是附着在 UI 上的一段文案而是界面状态的一部分。只要状态会变语义快照就要跟着变只要节点会复用标签的身份就不能只依赖下标只要有弹层焦点就必须能进、能出、能回到触发点。二、先把页面翻译成可比较的语义快照门禁工具没有尝试替代系统读屏。它做的是更窄、也更适合 CI 的事情进入约定页面收集当前可交互节点的角色、文本、描述、边界、可用状态和焦点关系然后在一次动作前后做差异比较。目录里最重要的不是测试用例数量而是职责分离AccessibilityAuditPage.ets负责演示状态SemanticCollector.ets负责采集A11yRuleSet.ets只做纯规则判断AuditReporter.ets输出审核材料。第一段代码解决“采到的节点每次顺序不同报告无法稳定比较”的问题。快照以语义身份排序而不是以遍历顺序排序同时把像素边界转换成 vp避免不同密度模拟器让同一触控目标得到两个结论。// SemanticCollector.etsexportinterfaceSemanticNode{key:stringrole:stringlabel:stringenabled:booleanboundsVp:[number,number,number,number]nextFocusKey?:string}exportasyncfunctioncollectSemanticSnapshot(page:string):PromiseSemanticNode[]{constrawawaituiInspector.query({page,interactiveOnly:true})constdensitydisplay.getDefaultDisplaySync().densityPixelsreturnraw.map((node)({key:node.accessibilityId||${node.role}:${node.resourceId},role:node.role,label:(node.accessibilityText||node.text||).trim(),enabled:node.enabled,boundsVp:node.bounds.map((px:number)Math.round(px/density))as[number,number,number,number],nextFocusKey:node.nextFocusId})).sort((a,b)a.key.localeCompare(b.key))}这里最容易犯的错是拿可见文本当唯一标签。图标按钮通常没有文本带角标的按钮还会把数字拆成另一个节点列表项复用后可见文本正确也不代表accessibilityText已更新。采集器因此保留“显式描述优先、文本兜底”的顺序并要求业务侧提供稳定的accessibilityId。采集发生在页面onPageShow完成、首帧布局稳定之后离开页面立即释放 Inspector 会话避免下一页沿用旧节点树。三、规则要能解释不只给红灯第二段代码把规则集中到lintNode。门禁不直接返回布尔值而是返回规则编号、节点身份和可操作的说明。触控目标采用 48 vp 门槛禁用控件仍保留标签因为“当前不可操作”也需要被读屏解释纯装饰元素在采集阶段已被排除避免用无意义描述凑通过率。// A11yRuleSet.etsexportinterfaceAuditIssue{rule:string;key:string;message:string}exportfunctionlintNode(node:SemanticNode):AuditIssue[]{constissues:AuditIssue[][]const[left,top,right,bottom]node.boundsVpif(!node.label){issues.push({rule:A11Y_LABEL_EMPTY,key:node.key,message:交互节点缺少可读标签})}if(right-left48||bottom-top48){issues.push({rule:A11Y_TARGET_SMALL,key:node.key,message:触控目标${right-left}×${bottom-top}vp小于 48×48vp})}if(node.nextFocusKeynode.key){issues.push({rule:A11Y_FOCUS_SELF_LOOP,key:node.key,message:焦点指向自身})}returnissues}重复标签不能在单节点规则里判断。我们按“同一可见区域、同一角色、同一标签”分组允许正文里出现多个“已完成”但不允许三个相邻按钮都叫“更多”。修复时订单按钮的标签改成“订单 20261003-18更多操作”既包含对象身份也保留动作。触控区域则用透明热区扩到 48 vp而不是把图标本身硬拉大。四、动态标签必须经过动作前后对账静态扫描通过后最隐蔽的问题仍然存在。AccessibilityAuditPage 里的退款按钮由orderState驱动视觉文字更新发生在状态提交后旧实现的无障碍标签却在组件创建时拼接一次。我们给工具加了“注入动态标签丢失”按钮故意恢复这个错误再由compareAfterAction执行动作、等待语义树稳定并核对变化。// AccessibilityAuditPage.etsStateprivateorderState:PAID|REFUNDEDPAIDStateprivateactionLabel:string订单 20261003-18申请退款privateasynccompareAfterAction():Promisevoid{constbeforeawaitcollectSemanticSnapshot(AccessibilityAuditPage)this.orderStateREFUNDEDthis.actionLabel订单 20261003-18查看退款awaitinspectorBridge.waitForStableTree(2,120)constafterawaitcollectSemanticSnapshot(AccessibilityAuditPage)constchangedsemanticDiff.changed(before,after,refund-action)hilog.info(0x1712,A11yGate,taskA11Y-1712 stateREVIEW_READY changed${changed}pass24/24)}Button(this.orderStatePAID?申请退款:查看退款).accessibilityText(this.actionLabel).width(120).height(48)等待策略没有使用固定一秒延迟。工具要求连续两次语义树哈希一致采样间隔 120 ms才认为状态稳定。这样既不会因为动画尚未结束误判也不把 CI 时间浪费在每个动作后的长等待。若页面在后台动作不会继续执行恢复前台后会重建会话并从该用例起点重跑避免拿半截状态生成审核材料。调试时HiLog 只保留能和报告对账的字段任务 ID、页面、节点数、规则编号、动作名和最终状态。示例里的关键日志是taskA11Y-1712 pageAccessibilityAuditPage nodes486修复完成后是stateREVIEW_READY pass24/24 unlabeled0 duplicate0 small0 cycle0。如果图片、报告和日志里任何一个数字不一致门禁不会允许导出。五、焦点环路比空标签更难发现那条焦点环路来自筛选弹窗。打开时焦点进入首个选项关闭时却回到一个已经销毁的临时节点框架随后选择页面第一个可聚焦元素读屏用户感觉像被突然扔回顶部。我们把触发按钮的语义 key 记录为filter-entry弹窗关闭后显式恢复焦点。弹窗自身只形成有限循环返回键始终能退出。这个修复也暴露出门禁的边界它能发现自环、断链和关闭后落点错误但不能判断播报语气是否自然也不能替代真实用户对复杂手势的体验。最终发布前仍保留人工抽检只是把人工从“找空标签”这种机械工作中解放出来集中检查读屏顺序、措辞和多指手势。工具最终把 24 个页面全部跑完486 个节点无标签从 7 降到 0重复标签从 3 组降到 0小目标从 5 个降到 0焦点环路从 1 条降到 0。任务状态进入 REVIEW_READY按钮“导出审核报告”才变为可用。六、把门禁放在提交之前而不是审核之后我们最后没有把它做成一个只在发布日运行的大脚本而是拆成两层。开发阶段只扫描改动页面十几秒就能给出错误发布流水线跑完整 24 页并生成 JSON、Markdown 和截图索引。规则版本和应用版本一起写入报告防止同一份结果在规则升级后仍被误用。还要特别处理重复调用Inspector 会话按页面单例持有新的扫描开始前先取消旧任务报告写入采用临时文件加原子替换避免两次“导出审核报告”产生半份文件。页面离开、Ability 进入后台或测试中止时监听器、定时器和浮层都必须释放。否则下一轮看似多出一个焦点节点实际是上轮调试浮层残留。在团队协作上我们还把规则编号写进缺陷模板。业务开发看到A11Y_TARGET_SMALL可以直接定位到节点 key、页面和 32×32 vp 的实测边界设计同学则能判断是扩大透明热区还是重新安排控件间距。对于确实无法达到 48 vp 的密集图表控制点必须提交带原因、替代手势和人工验证记录的例外而不是在代码里偷偷降低阈值。例外也有过期版本下一次大改版会自动重新进入检查队列。多语言是另一个容易漏掉的边界。中文标签不重复并不代表英文翻译后仍可区分。完整流水线会在中文和英文资源各跑一遍同时检查格式化参数是否真的进入accessibilityText。若订单号因为资源占位符错误丢失三个按钮就会在英文环境重新退化成相同的 “More actions”。这次最有价值的不是把 16 个问题改成 0而是建立了一套可重复的证据链页面状态变化会触发语义变化语义变化能被快照捕获规则能解释失败原因报告又能回到具体节点和日志。无障碍适配从发布前的清单项变成了和布局、性能一样可以持续回归的工程约束。

相关新闻

从调研报告到生产落地:Agent开发架构、LangGraph与并发稳定性指南

从调研报告到生产落地:Agent开发架构、LangGraph与并发稳定性指南

我从不觉得调研报告是什么高深的东西,直到我因为要选技术栈,连续翻了十几份Agent相关的开发者报告。说句实话,大部分报告都在讲正确废话,但2026年这份Agent开发者调研报告,配合阿里云那份《Alibaba Cloud AI Agent Han…

2026/10/9 0:30:46 阅读更多 →
LiveAgent安全设计解析:为什么你的API Key永远不会离开本机

LiveAgent安全设计解析:为什么你的API Key永远不会离开本机

LiveAgent安全设计解析:为什么你的API Key永远不会离开本机 【免费下载链接】LiveAgent A fully functional AI Agent desktop client that supports Webui access and can be creatively customized and expanded! 项目地址: https://gitcode.com/gh_mirrors/li/…

2026/10/9 0:30:46 阅读更多 →
AI芯片为何偏爱脉动阵列:从矩阵乘法到TPU的架构解析

AI芯片为何偏爱脉动阵列:从矩阵乘法到TPU的架构解析

1. 从矩阵乘法说起:为什么AI芯片偏偏看上了脉动阵列很多人第一次听到“脉动阵列”这个词,脑子里浮现的是一排排整齐的运算单元像心跳一样同步跳动的画面。这个直觉其实相当准确。要理解它为什么在AI芯片领域被反复提起,得先回到一个最朴素的问…

2026/10/9 0:29:46 阅读更多 →

最新新闻

4G显存也能流畅跑大模型:llama.cpp与GGUF量化实战指南

4G显存也能流畅跑大模型:llama.cpp与GGUF量化实战指南

如果你的电脑还在用 4GB 显存的显卡,比如 GTX 1650、RTX 3050 Laptop 或者 AMD 那边的 RX 6500 XT,想跑本地大模型,第一反应可能是到处找精简版、量化版,或者干脆放弃转用云 API。但今天我直接说结论:4G 显存完全能跑&…

2026/10/9 4:03:31 阅读更多 →
JavaScript作业避坑指南:函数、精度、Canvas与OC交互全解析

JavaScript作业避坑指南:函数、精度、Canvas与OC交互全解析

又到交JavaScript作业的季节了。我这些年帮学弟学妹和网上的朋友看过少说几百份作业代码,最深的感受是:大部分人不是不会写,而是压根没搞清楚题目到底想考什么。这篇东西是冲着"JavaScript作业"这几个字来的——不管你是卡在函数定…

2026/10/9 4:03:31 阅读更多 →
Django+Flask搭建机器人工厂管理系统:设备数据采集与生产管理实践

Django+Flask搭建机器人工厂管理系统:设备数据采集与生产管理实践

1. 项目背景与整体设计思路接这个项目之前,客户那边的生产车间里已经有十六台焊接机器人和四台搬运机器人,专门做汽车挡泥板的生产。挡泥板这东西听着不起眼,却是汽车底盘防护的关键件,产线上的机器人每天三班倒,大概要…

2026/10/9 4:03:31 阅读更多 →
字符编码与交叉变量初始化:线上乱码的原理与排查实战

字符编码与交叉变量初始化:线上乱码的原理与排查实战

1. 交叉变量初始化到底在解决什么问题前两天帮一个同事排查线上服务偶尔中文乱码的问题,代码跑了三四年没动过,部署方式也没变,结果新环境一上线,日志里的中文标题全部变成了一堆问号和乱码。查到最后,问题不是出在业务…

2026/10/9 4:03:31 阅读更多 →
Agent-Reach:面向业务意图的AI代理调度中枢

Agent-Reach:面向业务意图的AI代理调度中枢

1. 项目概述:Agent-Reach 是什么,它解决的不是“调用API”这个动作,而是“让AI代理真正抵达业务现场”的最后一公里问题Agent-Reach 这个名字乍看像某个新出的大模型工具或CLI套件,但如果你翻过最近三个月 Reddit 上 r/LocalLLMs、…

2026/10/9 4:03:31 阅读更多 →
Agent-Reach 深度解析:Python 构建 CLI 型 AI Agent 的工具调用与避坑指南

Agent-Reach 深度解析:Python 构建 CLI 型 AI Agent 的工具调用与避坑指南

1. 从"Agent-Reach"这个名字说起:它到底想解决什么问题第一次看到"Agent-Reach"这个项目名,我的直觉是:这大概率是一个围绕 AI Agent 能力边界扩展的工具,而不是又一个"套壳聊天机器人"。原因很简单…

2026/10/9 4:02:30 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →