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 导航。

把语义契约纳入组件评审

每个交互组件都可以用四个问题快速评审:

  1. 用户听到的名称是否说明目的,而不是外观?
  2. 角色与实际操作是否一致?
  3. 选中、开关、错误和加载状态是否可感知?
  4. 不使用触屏视觉提示,是否仍能完成操作?

当语义成为组件 API,UI 测试也会自然转向用户可观察行为。好的 Compose 无障碍设计不是维护一棵额外的树,而是确保视觉树和语义树描述的是同一个产品:看得见、听得懂、操作得了,也能够被稳定验证。