无障碍访问:ContentDescription、语义化、TalkBack支持

说实话,无障碍访问这个话题,在不少项目里都是「说起来重要,做起来次要」。我见过太多App,功能做得花里胡哨,但视障用户一打开,直接懵了——按钮没标签,图片没描述,焦点乱跳。嗯,这其实不是技术问题,是意识问题。

今天我们就聊聊Kotlin里怎么做无障碍。说白了,就是让你的App能被「听」懂。

为什么无障碍不是「加分项」而是「必选项」?

你可能觉得,视障用户占比不高,投入产出比不划算。但我想说一个数据:中国有超过1700万视障人士。这还不算那些暂时性障碍的用户——比如你开车时用语音操控手机,或者屏幕碎了看不清。

我在项目中遇到过最典型的场景:一个电商App的「加入购物车」按钮,图片上写了「立即购买」,但ContentDescription是空的。结果TalkBack读出来就是「按钮,未加标签」。用户根本不知道点下去会发生什么。

所以,无障碍不是慈善,是用户体验的一部分。

ContentDescription:给「看不见」的元素一个名字

ContentDescription是Android无障碍体系里最基础、也最容易被忽略的属性。它的作用很简单:为ImageView、ImageButton等非文本控件提供文字描述。

举个例子:

<ImageView
    android:id="@+id/ivAvatar"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:src="@drawable/ic_user"
    android:contentDescription="用户头像" />

在Kotlin代码里,你也可以动态设置:

ivAvatar.contentDescription = "当前登录用户头像"

这里有个坑——不要用「图片」「按钮」这种词。TalkBack会自动识别控件类型,你只需要描述「是什么」或「做什么」。比如「关闭弹窗」比「关闭按钮」更合适。

我曾经踩过的坑: 给一个装饰性分割线加了contentDescription,结果TalkBack每次都读「分割线」,用户烦得要死。后来才知道,纯装饰性的元素应该设置 contentDescription="@null",让TalkBack直接跳过。

语义化:让布局结构「可理解」

语义化这个词,做前端的同学应该不陌生。在Android里,它指的是用正确的控件类型表达正确的交互意图。

举个例子:一个可点击的「设置」图标,很多人会用ImageView加点击事件。但更好的做法是用ImageButton,因为TalkBack会自动识别它是「按钮」,并提示用户「可双击激活」。

再比如列表项,很多人喜欢用LinearLayout或RelativeLayout。但如果你用RecyclerView,配合item的role属性,TalkBack就能告诉用户「列表中有5项,当前是第3项」。

我建议你记住一个原则:能用系统控件,就别自定义。系统控件自带无障碍支持,你省事,用户也省心。

小技巧: 用 AccessibilityNodeInfo 的 setRoleDescription() 方法,可以自定义控件的角色描述。比如一个自定义的「开关」控件,你可以告诉TalkBack「这是一个开关,当前状态为开」。

TalkBack支持:从「能用」到「好用」

TalkBack是Android自带的屏幕阅读器。你不需要额外写代码,只要遵循无障碍规范,TalkBack就能正常工作。但「正常工作」和「体验良好」之间,差着几个细节。

我整理了一个清单,你可以对照检查:

检查项 说明 常见问题
焦点顺序 用 accessibilityTraversalBefore/After 控制 默认焦点顺序混乱,用户操作效率低
分组朗读 用 ViewGroup 的 focusable="true" 实现 列表项被拆成多个焦点,用户要滑动多次
动态内容 用 announceForAccessibility() 通知变化 页面更新了,TalkBack没反应
手势冲突 避免自定义手势覆盖TalkBack手势 用户无法双指滑动,功能失效

举个例子,动态内容通知:

// 当购物车数量变化时,通知TalkBack
binding.tvCartCount.text = "3"
binding.tvCartCount.announceForAccessibility("购物车中有3件商品")

你想想看,如果没有这行代码,用户点了一下「加入购物车」,页面数字变了,但TalkBack没任何反馈。用户会以为操作没成功,又点了一次——结果加了两次。

SVG:无障碍知识体系全景图

Android 无障碍访问 ContentDescription 为图片/按钮提供文字描述 装饰性元素设为 @null 避免「图片」「按钮」等冗余词 语义化布局 用正确控件表达交互意图 优先使用系统控件 自定义控件需设置 role TalkBack 支持 控制焦点顺序 分组朗读提升效率 动态内容主动通知 实践建议 1. 用 Accessibility Scanner 工具扫描问题 2. 打开 TalkBack 亲自体验一遍核心流程

实战:给一个登录页面做无障碍改造

光说不练假把式。我们拿一个常见的登录页面来实操。

改造前:

<EditText
    android:id="@+id/etUsername"
    android:hint="请输入用户名" />

<EditText
    android:id="@+id/etPassword"
    android:inputType="textPassword"
    android:hint="请输入密码" />

<Button
    android:id="@+id/btnLogin"
    android:text="登录" />

改造后:

<EditText
    android:id="@+id/etUsername"
    android:hint="请输入用户名"
    android:importantForAccessibility="yes"
    android:contentDescription="用户名输入框" />

<EditText
    android:id="@+id/etPassword"
    android:inputType="textPassword"
    android:hint="请输入密码"
    android:contentDescription="密码输入框,密码已隐藏" />

<Button
    android:id="@+id/btnLogin"
    android:text="登录"
    android:contentDescription="点击登录" />

你看,改动不大,但体验天差地别。密码框的contentDescription里加了「密码已隐藏」,用户就知道输入的内容不会被看到,心里踏实。

在Kotlin里,我们还可以做更多:

// 登录成功后通知
btnLogin.setOnClickListener {
    if (loginSuccess()) {
        btnLogin.announceForAccessibility("登录成功,正在跳转首页")
        // 跳转逻辑
    } else {
        btnLogin.announceForAccessibility("登录失败,请检查用户名和密码")
    }
}
核心原则: 无障碍不是「额外工作」,而是「正确的工作方式」。你写的每一行代码,都有可能被一个看不见屏幕的人「听」到。

避坑指南:我踩过的那些坑

  • 自定义View的焦点问题:我曾经写了一个自定义的「滑动验证」控件,结果TalkBack完全无法聚焦。后来发现需要重写 onInitializeAccessibilityNodeInfo() 方法,手动设置焦点信息。
  • 列表项复用导致朗读错乱:RecyclerView的item复用,如果contentDescription没及时更新,TalkBack会读到旧数据。解决方案是在 onBindViewHolder 里每次都重新设置。
  • 弹窗焦点陷阱:弹窗关闭后,焦点没有回到触发弹窗的按钮上,用户不知道当前在哪。用 requestFocus() 可以解决。

嗯,说了这么多,其实就一句话:把你的App当成一个「只能听」的产品来设计。你试试关掉屏幕,只用TalkBack操作一遍你的App,保证能发现一堆问题。

我个人习惯在项目提测前,专门留半天时间做无障碍自测。虽然花时间,但每次都能发现几个漏掉的contentDescription或者焦点顺序问题。久而久之,团队里就形成了习惯,新功能上线前都会过一遍无障碍检查。

好了,这一章的内容就到这里。记住,无障碍不是终点,而是起点。你的用户里,总有人需要这份「被看见」的尊重。

公众号:蓝海资料掘金营,微信 deep3321