Jetpack Compose 无障碍实战:让语义树同时服务用户与测试
Compose 界面在视觉上正确,不代表辅助技术能够理解。屏幕阅读器、Switch Access 和 UI 测试面对的不是像素,而是与组合树并行生成的语义树。它描述控件的名称、角色、状态和可执行操作,也决定自动化测试能找到什么。
因此,无障碍不是发布前补几条 contentDescription。更可靠的做法是把语义当作组件公开接口:视觉实现可以调整,但用户和测试依赖的含义必须稳定。
优先使用自带语义的组件
Button、Checkbox、Switch 和 RadioButton 已经提供角色、状态与交互语义。用一个带 clickable 的 Box 重画按钮,往往会遗漏角色、按压范围、键盘操作或禁用状态。
图标只是装饰时,应让它不产生重复播报:
Button(onClick = onSave) {
Icon(
imageVector = Icons.Default.Save,
contentDescription = null,
)
Text("Save")
}
按钮中的文字已经表达操作,图标不需要再读一次。只有独立图标按钮才需要简洁、面向动作的名称,例如“删除草稿”,而不是描述外形的“垃圾桶图标”。
自定义控件要给出名称、角色和状态
一个自绘难度选择卡片应让辅助技术知道它可以选择、当前是否选中,而不是只暴露内部多个 Text:
Row(
modifier = Modifier
.selectable(
selected = selected,
onClick = onSelect,
role = Role.RadioButton,
)
.semantics {
contentDescription = label
stateDescription = if (selected) "Selected" else "Not selected"
}
) {
DifficultyArtwork()
Text(label)
}
真实项目应把状态文案放进 string resource,以便本地化。能用 selectable、toggleable 或 clickable 时,优先使用这些高层 modifier;它们会同时设置动作与相应语义,比手工拼装更不容易产生“能播报但不能操作”的控件。
语义粒度决定导航成本
卡片里包含标题、说明、时间和按钮时,默认语义可能产生过多焦点。semantics(mergeDescendants = true) 可以把逻辑上属于一个操作的子元素合成一个节点;父级 clickable 也通常会合并后代。
但合并不是越多越好。如果卡片内还有收藏按钮,整卡合并会隐藏独立操作。把主要卡片与次要动作保持为两个合理焦点,通常比“一次读完所有内容”更清晰。clearAndSetSemantics 会丢弃后代信息,只应在需要完整替换语义时使用。
调试时先打印语义树:
composeTestRule.onRoot().printToLog("Semantics")
测试默认使用合并后的树。节点找不到时,可以查看未合并树来理解组件实际暴露了什么,而不是立即加一个只为测试存在的 testTag。
不要只用颜色表达状态
错误、选中、成功和不可用状态需要文字、图标形状或可读状态共同表达。高对比度并不能帮助无法区分颜色含义的用户。动态状态变化还应更新 stateDescription,必要时通过 live region 让重要结果被播报,但避免频繁信息造成干扰。
同时检查字体放大、触控目标、横竖屏和大屏窗口。固定高度加单行文字很容易在大字体下裁切;仅靠精确手势完成的操作需要可替代的无障碍动作。
测试用户可观察的语义
Compose 测试通过语义定位节点。断言角色、状态和动作,比断言内部布局层级更能抵抗重构:
composeTestRule
.onNode(
hasText("Hard") and
hasRole(Role.RadioButton)
)
.assertIsNotSelected()
.performClick()
.assertIsSelected()
如果测试只能依靠“第三个 Row 的第二个 Text”,组件对辅助技术通常也缺少清晰身份。自定义语义属性适合日期选择器等标准属性无法表达的业务值,不应拿来暴露颜色、圆角或字号等纯视觉实现。
自动检查与人工体验缺一不可
从 Compose 1.8.0 起,官方无障碍测试支持接入 Accessibility Test Framework。加入 ui-test-junit4-accessibility 后,可以在规则上启用检查并主动扫描根节点:
composeTestRule.enableAccessibilityChecks()
composeTestRule.onRoot().tryPerformAccessibilityChecks()
自动检查可以发现部分对比度、触控目标或遍历顺序问题,但不能判断播报是否自然、流程是否完成得了。发布前还应实际开启 TalkBack 和 Switch Access,完成核心任务,并测试大字体、显示缩放、深浅主题和键盘或 D-pad 导航。
把语义契约纳入组件评审
每个交互组件都可以用四个问题快速评审:
- 用户听到的名称是否说明目的,而不是外观?
- 角色与实际操作是否一致?
- 选中、开关、错误和加载状态是否可感知?
- 不使用触屏视觉提示,是否仍能完成操作?
当语义成为组件 API,UI 测试也会自然转向用户可观察行为。好的 Compose 无障碍设计不是维护一棵额外的树,而是确保视觉树和语义树描述的是同一个产品:看得见、听得懂、操作得了,也能够被稳定验证。