Administrator
发布于 2026-08-25 / 27 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 37 篇:Android 无障碍开发:构建包容性应用

上一篇对比了图片加载框架,这篇聊一个被大量开发者忽略但越来越重要的主题——无障碍(Accessibility)。Google Play 对无障碍有明确要求,国内也在逐步推进信息无障碍标准。这不是"做了锦上添花"的事,是"不做会被拒"的事。

为什么需要无障碍

全球有超过 10 亿残障人士,其中视力障碍约 2.5 亿。在 Android 上,TalkBack 屏幕阅读器是最常用的辅助工具。你的 App 如果没有做好无障碍适配,视障用户可能完全无法使用。

除了社会责任,无障碍适配还有实际好处:

  • Google Play 的无障碍检查可能影响推荐

  • 某些地区法规要求(美国 ADA、欧盟无障碍法案)

  • 做好无障碍的 UI 代码质量通常更高(语义化标签、合理焦点顺序)

TalkBack 怎么工作

TalkBack 通过 AccessibilityService 监听界面上的 AccessibilityNodeInfo,把内容读给用户。用户通过触摸屏幕探索元素,TalkBack 朗读出触摸位置的元素描述。

基本流程:用户触摸某个 View → TalkBack 获取该 View 的 contentDescription 或 text → 朗读出来 → 用户双击激活。

基础适配

1. 图片要有 contentDescription

<!-- 有意义的图标 -->
<ImageView
    android:contentDescription="返回"
    android:src="@drawable/ic_back" />

<!-- 纯装饰性图片 -->
<ImageView
    android:importantForAccessibility="no"
    android:src="@drawable/ic_decoration" />

contentDescription 要描述图片的"功能"而不是"外观"。比如"返回"而不是"左箭头图标"。

2. 按钮文字要明确

<!-- 不推荐:TalkBack 会读"更多" -->
<ImageButton android:src="@drawable/ic_more" />

<!-- 推荐 -->
<ImageButton
    android:contentDescription="更多选项"
    android:src="@drawable/ic_more" />

3. 焦点顺序

默认焦点顺序是 XML 中从上到下、从左到右。但如果你用了复杂布局(Frame 叠加、RecyclerView 嵌套),可能需要手动控制:

<View
    android:id="@+id/title"
    android:focusable="true"
    android:nextFocusDown="@id/subtitle" />

<View
    android:id="@+id/subtitle"
    android:focusable="true"
    android:nextFocusUp="@id/title" />

4. 触摸目标不小于 48dp

Material Design 规范要求可交互元素最小 48x48 dp。太小了视障用户和手部不便的用户很难准确点击。

Compose 的无障碍

@Composable
fun MyButton() {
    Box(
        modifier = Modifier
            .size(48.dp)
            .clickable { onClick() }
            .semantics {
                contentDescription = "返回"
                role = Role.Button
            }
    ) {
        Icon(Icons.Default.ArrowBack, contentDescription = null)
    }
}

Compose 中无障碍通过 semantics 修饰符配置。注意 Icon 的 contentDescription 如果外层已经设了语义,内部设为 null 避免重复朗读。

合并语义

多个元素描述一件事时,可以合并为单个语义节点:

Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        contentDescription = "张三,3 条未读消息"
    }
) {
    Image(...) // 头像
    Text("张三") // 名字
    Text("3 条未读") // 状态
}

不合并的话 TalkBack 会分别朗读"头像"、"张三"、"3 条未读",用户体验差。

自定义 View 的无障碍

class RatingBar : View {
    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        info.contentDescription = "$currentStar 星评分,最高 5 星"
        info.addAction(AccessibilityAction.ACTION_CLICK)
    }
}

复杂交互的自定义 View 还需要实现 performAccessibilityAction 来响应 TalkBack 的操作。

实时区域(Live Region)

需要通知用户内容变化时,使用 liveRegion:

<TextView
    android:accessibilityLiveRegion="polite"
    android:text="倒计时:5" />

polite 会在当前朗读结束后播报变化,assertive 会打断当前朗读。适合聊天消息、错误提示等动态内容。

测试无障碍

工具

  1. Accessibility Scanner(Google 官方 App):扫描截图,报告文字对比度、触摸目标大小、缺失标签等问题。

  2. TalkBack:开启 TalkBack 实际操作 App,体验视障用户的流程。

  3. Android Studio 的 Accessibility Check:在 Layout Inspector 中查看无障碍问题。

  4. Espresso 测试:

@Test
fun testButtonAccessibility() {
    onView(withContentDescription("返回"))
        .check(matches(isDisplayed()))
}

检查清单

  • 所有可交互元素都有可朗读的文字描述

  • 装饰性图片标记为 importantForAccessibility="no"

  • 触摸目标 >= 48dp

  • 文字与背景色对比度 >= 4.5:1(WCAG AA 标准)

  • 焦点顺序符合视觉阅读顺序

  • 动态内容变化通过 Live Region 通知

小结

无障碍适配不是花哨的技术,更多是"细心"和"同理心"。核心就几条:该有描述的地方加描述,该合并的合并,该忽略的标记忽略,确保视障用户能顺畅完成核心操作。做好无障碍,所有用户体验都会提升。

下一篇聊聊 Android 启动优化进阶:Baseline Profiles 和 Macrobenchmark。


评论