ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 ArkUI无障碍:动态清单朗读焦点续接与语义分组

HarmonyOS 7 ArkUI无障碍:动态清单朗读焦点续接与语义分组 这次问题不是崩溃也不是某个按钮点不动。上架预检报告只写了几项很“软”的异常筛选任务后朗读焦点跳到页面标题三枚图标按钮没有可读名称一条任务被标题、截止时间和状态拆成三次播报。正常触控用户几乎察觉不到可一旦打开屏幕朗读完成一条清单要横扫六七次。Demo 叫FocusLedger页面是AccessibilityAuditPage我把这次修复的任务号定为A11Y-1818。一、报告里的四个数字背后是同一个语义树问题初始页面有 24 个可访问节点预检记录为缺失标签 3、重复播报 2、筛选后焦点跳失 4、孤立操作 1综合分 68。看起来像四类问题实际都来自“视觉组件树直接等同于无障碍语义树”一张任务卡里有标题、标签、时间、复选图标、更多图标视觉上分开合理朗读时却不应该每个子组件都成为独立停靠点。修复目标没有写成“属性补全”而是一个可验收状态机BASELINE_FAIL → SEMANTICS_PATCHED → FOCUS_RESTORED → PRECHECK_PASS。筛选词固定为“未完成”筛选前焦点落在CHK-07筛选后它仍存在于结果第 3 位就应该继续成为首个可读任务而不是让用户从页面顶部重新探索。18:18 的最终报告必须同时满足labels0、duplicates0、focusJumps0、orphanActions0、score100。HarmonyOS 当前 ArkUI 无障碍属性覆盖分组、文本、说明、重要性、首焦点和下一个焦点。官方文档说明启用accessibilityGroup后组件及其子组件会作为一个整体设置accessibilityText时屏幕朗读优先播报该文本accessibilityDescription用于补充操作后果accessibilityLevel决定节点是否可被辅助服务识别。无障碍属性参考。这几个属性不是越多越好关键是先决定一张卡到底要成为一个节点还是多个节点。二、目录按“语义、焦点、证据”拆而不是按页面堆代码FocusLedger的pages/AccessibilityAuditPage.ets只拼装界面components/ChecklistRow.ets定义任务卡语义a11y/FocusContinuation.ets保存稳定焦点键audit/SemanticsProbe.ets汇总预检规则model/ChecklistItem.ets保证任务 ID 不跟数组下标走。这样筛选、排序和局部刷新不会让语义策略散落在多个ForEach回调里。第一段代码解决“一张卡被拆成多次播报”。整行设置为无障碍组播报文本由业务字段显式生成装饰图标不进入语义树复选按钮仍保留为独立操作并给出操作说明。这里不能简单把整行所有子节点都设成no否则用户听得到任务信息却无法完成任务。Componentexportstruct ChecklistRow{Propitem:ChecklistItem;ProprestoreId:string;onToggle:(id:string)void(){};build(){Row({space:12}){Column({space:4}){Text(this.item.title)Text(${this.item.dueText}·${this.item.done?已完成:未完成})}.layoutWeight(1).accessibilityLevel(no-hide-descendants)Button(this.item.done?撤销完成:标记完成).id(action_${this.item.id}).accessibilityText(${this.item.title}${this.item.done?撤销完成:标记完成}).accessibilityDescription(双击后更新任务状态).accessibilityLevel(yes).onClick(()this.onToggle(this.item.id))}.id(task_${this.item.id}).accessibilityGroup(true,{accessibilityPreferred:true}).accessibilityText(${this.item.title}${this.item.dueText}${this.item.done?已完成:未完成}).accessibilityDefaultFocus(this.item.idthis.restoreId)}}accessibilityPreferred: true让分组优先采用显式无障碍文本而不是把视觉子节点按深度顺序随意拼接。复选按钮用accessibilityLevel(yes)逃离父分组约束这是官方文档明确支持的边界。需要注意按钮的无障碍文本包含任务标题否则连续滑到多个“标记完成”时用户无法知道正在操作哪一项。页面销毁不需要释放这些声明式属性但必须清掉后面用于延迟恢复首焦点的计时器。三、焦点续接不能记录数组下标要记录业务身份原实现保存focusedIndex6。筛选后列表只剩 9 项第 6 项已经换成另一条任务系统只能回到第一个可聚焦节点。FocusLedger 改为记录CHK-07。过滤完成后先检查该 ID 是否仍存在存在就恢复它不存在则在原排序中向后找最近的未完成项再没有才回退到筛选结果第一项。这个策略比“永远跳到第一个”多了几行却保住了用户的探索上下文。第二段代码解决筛选与异步渲染的时序。accessibilityDefaultFocus必须在新节点树建立前确定因此恢复键先写入状态再提交过滤结果。代次filterEpoch防止快速切换筛选条件时较早的计算结果覆盖较新的焦点计划。exportclassFocusContinuation{privatefilterEpoch0;privatelastFocusedId;remember(id:string):void{this.lastFocusedIdid;}asyncapplyFilter(all:ChecklistItem[],query:string):PromiseFilterResult{constminethis.filterEpoch;constfilteredawaitTaskFilter.run(all,query);if(mine!this.filterEpoch){thrownewError(STALE_FILTER:${mine});}constexactfiltered.find((item)item.idthis.lastFocusedId);constfallbackexact??filtered.find((item)!item.done)??filtered[0];return{items:filtered,restoreId:fallback?.id??,restorePosition:fallback?filtered.findIndex((item)item.idfallback.id)1:0};}invalidate():void{this.filterEpoch1;}}STALE_FILTER不是页面错误它表示用户的第二次筛选已经胜出。UI 只记录一条STALE_FILTER_DROPPED不弹 Toast也不清空当前列表。项目边界上如果当前焦点项被用户删除恢复到最近未完成项是产品规则不是系统默认其他产品可能更适合回到删除位置的下一项。关键是规则必须基于稳定 ID并在自动化测试里固定下来。四、把上架预检变成可重复的本地账本只在屏幕朗读里“听起来差不多”不够。SemanticsProbe为每个业务行登记预期节点行摘要、操作节点、稳定 ID、是否为恢复目标。它不试图替代系统无障碍服务而是先抓最容易回归的业务约束空标签、重复摘要、没有归属的操作、恢复目标缺失。真正的上架预检仍通过 DevEco Testing 运行本地账本负责在提交前挡住低级回归。第三段代码解决报告可追踪性。每次筛选完成生成一份AuditSnapshot只有页面代次仍有效且四项均为 0 才允许状态进入PRECHECK_PASS。离开页面时dispose()会使旧报告失效避免上一个页面实例的 100 分覆盖新页面的基线结果。typeAuditSnapshot{taskId:string;nodeCount:number;missingLabels:number;duplicates:number;focusJumps:number;orphanActions:number;score:number;};exportclassSemanticsProbe{privatepageEpoch1;audit(rows:SemanticRow[],restoreId:string,taskId:string):AuditSnapshot{constlabelsrows.map((row)row.spokenText.trim());constmissingLabelslabels.filter((text)text.length0).length;constduplicateslabels.length-newSet(labels).size;constorphanActionsrows.filter((row)!row.ownerId||!row.actionText).length;constfocusJumpsrestoreId!rows.some((row)row.ownerIdrestoreId)?1:0;constdeductionsmissingLabels*8duplicates*4orphanActions*6focusJumps*10;return{taskId,nodeCount:rows.length,missingLabels,duplicates,focusJumps,orphanActions,score:Math.max(0,100-deductions)};}canCommit(epoch:number,report:AuditSnapshot):boolean{returnepochthis.pageEpochreport.missingLabels0report.duplicates0report.focusJumps0report.orphanActions0;}currentEpoch():number{returnthis.pageEpoch;}dispose():void{this.pageEpoch1;}}这里的分数只是项目内门禁不伪装成官方审核分。官方测试体系把 UX、兼容性、稳定性等维度分开DevEco Testing 的上架预检基于应用上架质量标准给出自动化检测报告。DevEco Testing 资源与上架预检应用测试概述。本地分数的意义是让同一缺陷在开发机、CI 和提审前都有同样的名字与计数。五、调试时我只盯四条日志不盯“感觉顺了”本轮固定日志为taskA11Y-1818 nodes24、baseline labels3 duplicates2 focusJumps4 orphanActions1 score68、restore idCHK-07 position3 query未完成、final labels0 duplicates0 focusJumps0 orphanActions0 score100、statePRECHECK_PASS。如果 24 个节点突然变成 30说明视觉子节点又泄漏进语义树如果分数为 100 但恢复 ID 为空说明门禁只检查了标签没有检查导航连续性。IDE 图中左侧是FocusLedger的语义、焦点和审计目录中间停在ChecklistRow.ets的分组代码右侧模拟器展示同一任务号与 68→100 的修复结果底部 HiLog 使用上述五条日志。红色标注只圈accessibilityGroup、CHK-07恢复点和四项归零避免把图做成满屏审查意见。六、最终运行页必须让测试人员一眼复核18:18 的AccessibilityAuditPage显示状态PRECHECK_PASS。筛选词“未完成”命中 9 项CHK-07恢复到第 3 位24 个可访问节点中缺失标签、重复播报、焦点跳失和孤立操作全部为 0项目内预检分由 68 提升到 100。页面还保留“运行语义扫描”和“复测焦点续接”两个按钮测试人员不必返回 IDE 才能重新制造场景。手机图不是把 100 分放大做海报而是把恢复 ID、位置、筛选词和四项计数并列出来。状态栏时间、5G、Wi‑Fi、信号、电量图标与数字完整保留画面本身就是 9:16 屏幕不套手机外壳。对这类审核问题证据越具体后续版本越容易知道到底是哪条语义规则回退了。七、几个容易被“全绿”掩盖的边界第一中文朗读文本不要直接拼内部状态码PENDING应转换成“未完成”数量、日期和货币也应使用面向用户的本地化表达。第二动态列表里ForEach必须使用稳定业务键若仍用数组下标焦点续接代码再正确也会绑定到错误节点。第三分组并不代表越少焦点越好可独立执行的收藏、删除或展开操作仍应成为可识别节点只是要补充对象名称和后果说明。第四页面进入后台时不要主动抢回焦点。只有筛选、排序、删除等由用户触发且导致节点树替换的操作才执行一次续接计划。第五自动化扫描不能替代真实屏幕朗读复测尤其是长文本截断、语速下的理解成本和连续滑动顺序。第六accessibilityDefaultFocus只能有一个恢复目标多个列表分区同时标记会产生不可预测的首焦点竞争。八、这次修复的不是四个属性而是一条探索路径无障碍缺陷常被误解成“给图标加描述”。实际用下来更难的是状态变化之后用户能不能接着刚才的位置继续操作。FocusLedger 最终把视觉树、语义树、焦点身份和预检证据分开管理卡片负责说清楚自己稳定 ID 负责续接上下文代次阻止旧筛选回写审计账本把回归变成数字。PRECHECK_PASS因而不只是页面上的绿色状态而是一条能从代码、日志、手机运行页到上架预检报告互相核对的证据链。
返回列表