尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

HarmonyOS 7 ArkUI Accessibility + Hvigor:无障碍语义、触控热区与动态播报的提审门禁【鸿蒙心迹】

HarmonyOS 7 ArkUI Accessibility + Hvigor:无障碍语义、触控热区与动态播报的提审门禁【鸿蒙心迹】 页面看起来没有任何问题读屏打开后却变成了另一套产品顶部三个图标都被念成“按钮”筛选完成没有提示右下角的收藏按钮只有 32 vp手指稍微偏一点就点不中。这不是视觉走查能发现的问题。这次我把修复过程做成了一个小工程A11yGate Lab。它不负责替代人工无障碍测试而是把最容易反复出现的错误提前挡在构建阶段。审计编号固定为a11y_20261001_14页面共有 38 个可访问节点标签覆盖 38/38最小触控热区 48 vp语义分组 6 处动态播报 2/2最终问题数 0状态PASS。17:16 的运行结果和构建报告使用同一份数据。这篇不是逐项抄无障碍属性。真正要解决的是三个工程问题组件改版后语义不能跟着丢小图标视觉尺寸不变但可点击范围必须合格异步任务完成时读屏用户要收到一次、且只收到一次通知。一、审核反馈里的三个问题其实来自同一个缺口最初的商品筛选页已经通过常规功能测试。返回、清除条件和收藏都能点击加载状态也能正确结束。但无障碍走查给了三个明确问题图标按钮没有稳定名称收藏按钮的命中区域不足“筛选完成共 26 个结果”只更新了视觉文本没有触发动态播报。这些问题不是开发者不知道某个属性而是项目里没有统一的语义入口。有人直接给Image加onClick有人用 32×32 的Button包图标还有人把请求状态和 Toast 混在一起。只修当前页面下次换一套图标或复制一个列表项同样的问题还会回来。我先把页面的可访问信息拆成四类控件名称回答“这是什么”描述说明“执行后会怎样”层级决定它是否进入可访问树分组决定一组子节点该合并朗读还是逐个聚焦。触控尺寸则不属于朗读语义但必须和组件封装一起处理否则视觉与交互会继续分叉。二、图标可以是 24 vp点击热区不能跟着只有 24 vp这段代码解决的是“图标按钮在不同页面被重复手写标签、描述和热区很容易遗漏”。我把可点击图标收进A11yIconButton图标仍然是 24 vp外层统一为 48×48 vp并强制调用方提供资源化标签。exportinterfaceA11yIconButtonOptions{icon:Resource label:ResourceStr description?:ResourceStronClick:()void}BuilderexportfunctionA11yIconButton(options:A11yIconButtonOptions){Button({type:ButtonType.Circle}){Image(options.icon).width(24).height(24).accessibilityLevel(no)}.width(48).height(48).backgroundColor(Color.Transparent).accessibilityText(options.label).accessibilityDescription(options.description??).accessibilityLevel(yes).onClick(options.onClick)}这里把内部Image设为不单独聚焦是为了避免读屏先念图片、再念按钮造成重复真正承担交互语义的是外层Button。标签使用ResourceStr因此随语言资源变化不在组件中写死中文。48 vp 是命中热区不代表图标要放大视觉稿仍然可以保留轻量尺寸。这个 Builder 只负责基础动作。开关、单选、滑块等有状态控件不能一律套成普通按钮它们需要向系统暴露当前选中值。正式项目还要为禁用态、加载态和重复点击加约束如果组件离开页面前仍持有异步回调业务层应取消任务不能让已经不可见的按钮继续修改页面状态。三、读屏顺序不是布局顺序的副产品页面第二个问题来自商品卡片。视觉上先看到图片再看到名称、价格、优惠标签和收藏图标如果每个文本都进入可访问树一张卡片要横跳六次才能离开。我的处理不是随便把整卡合并而是按动作边界分组商品信息作为一组朗读收藏仍然保留独立按钮。A11yGate Lab最终有 6 个语义组焦点顺序是 1→8。顶部返回和页面标题在前筛选条件区随后商品信息与收藏动作成对出现底部结果摘要最后。装饰图标退出可访问树但价格、折扣和售罄状态不会因为“减少节点”而被隐藏。我用“只听不看”的方式重新走了一次流程从标题进入筛选修改价格区间提交再浏览第一张商品卡。焦点每次移动都能回答当前位置和下一步动作没有跳到屏幕外的缓存节点。列表滚动复用后我又返回上一项确认朗读内容已经随新的assetId更新而不是残留上一张卡片的商品名。这类复用错误在静态截图里完全看不出来。这里最容易犯的错是把父容器设为可访问后仍让所有子节点可聚焦。读屏会同时拿到父级拼接文本和子级文本用户听到重复内容。另一个极端是使用隐藏后代的设置把整个自定义组件吞掉视觉上的按钮仍可点击读屏却再也找不到。分组不是节点越少越好而是一次朗读是否表达一个完整、可操作的意思。四、把容易量化的问题放进 Hvigor 门禁人工测试能判断文案是否自然却不适合每次提交都数热区。下面这段脚本解决的是“组件重构后缺标签和小热区又悄悄混进主分支”。A11yRuleScanner扫描 ArkTS 源码的语法树定位带点击行为的组件并检查无障碍标签与尺寸链。exportfunctionscanA11yRules(files:SourceFile[]):A11yIssue[]{constissues:A11yIssue[][]for(constfileoffiles){walkArkTs(file,node{if(!hasModifier(node,onClick))returnif(!hasModifier(node,accessibilityText)){issues.push(issue(file,node,A11Y_LABEL_MISSING))}constsizeresolveTouchSize(node)if(size.width48||size.height48){issues.push(issue(file,node,A11Y_TOUCH_TARGET,${size.width}x${size.height}vp))}if(hasDecorativeChild(node)!decorativeChildIsHidden(node)){issues.push(issue(file,node,A11Y_DUPLICATE_NODE))}})}returnissues}扫描器不靠简单字符串搜索。链式属性可能换行尺寸也可能来自常量语法树至少能保证节点和修饰符属于同一个组件。resolveTouchSize只解析项目允许的常量表达式无法确定的动态值会报NEEDS_REVIEW不会武断地判定通过。它接入 Hvigor 的预提交任务后最初找出了 3 个问题两个图标缺少标签一个热区只有 32×32 vp。修复后报告变成 nodes38、labels38/38、minTouch48vp、groups6、issues0构建状态才允许进入PASS。这类门禁要控制误报仅凭源码无法判断文字是否准确也无法证明真实焦点顺序所以脚本结果必须和模拟器走查配合。报告采用稳定的规则编号和源码位置CI 只对本次新增问题失败历史问题进入明确的基线清单。基线不是免责清单它记录负责人和到期时间否则团队为了先通过构建而批量忽略告警门禁很快就会失去信用。规则升级时先在只报告模式运行一轮确认误报可控再切成阻断模式。DevEco Studio 中左侧不是通用示例目录而是components / accessibility / audit / model四条链路中间打开A11yIconButton.ets能看到 48 vp 热区和accessibilityText右侧运行A11yGate Lab底部 HiLog 打印audita11y_20261001_14、labels38/38、announcements2/2、issues0和focusOrder1-8。代码、运行页面和门禁报告使用同一组标识避免出现截图好看但无法复现的问题。五、动态播报要去重也要跟页面生命周期绑定静态标签修好以后筛选请求仍有一个隐蔽问题加载结束只改变了结果数量。视觉用户能看见“26 个结果”读屏焦点却还停在筛选按钮上没有任何反馈。如果每次状态刷新都播报又会在列表分页或重复渲染时连续打断用户。这段代码解决的是“异步结果完成后只播报一次旧请求回调不抢新请求的话筒”。页面为每次查询生成递增 token播报器用 key 去重并通过适配层发送 Accessibility Kit 事件。exportclassAccessibilityAnnouncer{privatelastKey:stringconstructor(privatereadonlybridge:AccessibilityEventBridge){}asyncannounceOnce(key:string,text:string):Promisevoid{if(keythis.lastKey)returnthis.lastKeykeyawaitthis.bridge.announce(text)}reset():void{this.lastKey}}privateasyncrunFilter():Promisevoid{consttokenthis.queryTokenthis.loadingtrueconstresultawaitthis.repository.query(this.filters)if(token!this.queryToken||!this.isPageActive)returnthis.itemsresult.itemsthis.loadingfalseawaitthis.announcer.announceOnce(filter_${token}_completed,筛选完成共${result.total}个结果)}适配层存在的意义是隔离不同 API 版本下事件参数和调用方式页面不直接拼平台事件对象。数据变化顺序也很重要先把items和loading更新为最终值再发完成播报否则用户听到完成后立即触摸列表拿到的仍可能是旧节点。播报文本也不能照搬视觉 Toast。Toast 可以写“成功”但读屏用户不知道什么成功这里必须包含动作和结果数量。失败时则保留可执行建议例如“网络不可用筛选未更新可重试”而不是只读错误码。对于高频进度不逐百分比播报只在开始、关键节点和完成时通知避免每秒打断一次当前焦点内容。页面进入后台时把isPageActive设为 false并取消能取消的查询重新进入后只有用户新触发的任务才允许播报。reset()只在页面会话真正结束时调用不能跟每次重绘绑定否则同一个完成事件会再次发出。正式项目还要处理网络重试、空结果和错误信息错误播报不能和成功播报共用一个模糊 key。六、17:16 的结果页不只展示一个绿色通过我没有把手机页做成“PASS”大字海报因为通过本身缺少诊断价值。页面同时展示节点总数、标签覆盖、最小热区、语义分组、动态播报和剩余问题下面还保留焦点顺序 1→8。任何一个数字回退都能快速定位是组件封装、扫描规则还是页面结构发生了变化。17:16 的纯屏幕截图里审计任务仍是a11y_20261001_1438 个节点标签 38/38最小触控热区 48 vp分组 6动态播报 2/2问题 0结果PASS。红色批注只指出“朗读标签完整”和“最小热区 48 vp”没有遮住页面实际数据。通过以后我又做了三次反向验证。去掉收藏按钮标签Hvigor 任务立即报告A11Y_LABEL_MISSING把外层尺寸改回 32 vp报告变成A11Y_TOUCH_TARGET: 32x32vp连续触发同一完成 keyHiLog 只出现一次 announcement。门禁不是把数字写死成漂亮结果而是能在故意破坏代码时可靠失败。七、能自动检查的是底线真正可用仍要靠人听静态规则适合检查“有没有”和“够不够大”不能回答“读起来是否符合人的预期”。同一个“更多”放在十张商品卡片上技术上有标签使用上仍然含糊应该包含上下文例如“更多星空台灯”。多语言资源也要在目标语言下实际朗读不能只确认 key 存在。还有几类边界留给专项测试系统字体放大后是否遮挡但仍可聚焦折叠屏窗口变化后焦点是否跳回顶部自绘Canvas或XComponent是否补充了可访问节点弹窗关闭后焦点是否回到触发按钮列表复用时语义是否跟着数据更新。电视、平板和手机的操作方式不同48 vp 也不是所有设备形态的唯一答案。我还把审计数据限定为构建产物的一部分而不是上传真实用户的朗读内容。门禁需要节点数量、规则编号和源码位置不需要采集用户点击过什么商品。开发日志在 release 构建中关闭详细文本只保留匿名计数这既减少隐私风险也避免把资源文案和业务数据混进长期日志。最终我把A11yIconButton、语义分组约定、AccessibilityAnnouncer和 Hvigor 扫描任务一起提交。只交一个脚本会漏掉运行时行为只改几个属性又挡不住回归。现在PASS代表一条完整链路资源化标签进入组件、触控热区达到底线、动态状态被正确播报、静态问题在构建前被发现最后再由真实读屏走查确认体验。参考资料HarmonyOS ArkUI 无障碍属性HarmonyOS Accessibility Kit 简介HarmonyOS ArkUI Native 无障碍能力HarmonyOS Hvigor 构建工具
返回列表