无障碍访问: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会自动识别控件类型,你只需要描述「是什么」或「做什么」。比如「关闭弹窗」比「关闭按钮」更合适。
语义化:让布局结构「可理解」
语义化这个词,做前端的同学应该不陌生。在Android里,它指的是用正确的控件类型表达正确的交互意图。
举个例子:一个可点击的「设置」图标,很多人会用ImageView加点击事件。但更好的做法是用ImageButton,因为TalkBack会自动识别它是「按钮」,并提示用户「可双击激活」。
再比如列表项,很多人喜欢用LinearLayout或RelativeLayout。但如果你用RecyclerView,配合item的role属性,TalkBack就能告诉用户「列表中有5项,当前是第3项」。
我建议你记住一个原则:能用系统控件,就别自定义。系统控件自带无障碍支持,你省事,用户也省心。
TalkBack支持:从「能用」到「好用」
TalkBack是Android自带的屏幕阅读器。你不需要额外写代码,只要遵循无障碍规范,TalkBack就能正常工作。但「正常工作」和「体验良好」之间,差着几个细节。
我整理了一个清单,你可以对照检查:
| 检查项 | 说明 | 常见问题 |
|---|---|---|
| 焦点顺序 | 用 accessibilityTraversalBefore/After 控制 | 默认焦点顺序混乱,用户操作效率低 |
| 分组朗读 | 用 ViewGroup 的 focusable="true" 实现 | 列表项被拆成多个焦点,用户要滑动多次 |
| 动态内容 | 用 announceForAccessibility() 通知变化 | 页面更新了,TalkBack没反应 |
| 手势冲突 | 避免自定义手势覆盖TalkBack手势 | 用户无法双指滑动,功能失效 |
举个例子,动态内容通知:
// 当购物车数量变化时,通知TalkBack
binding.tvCartCount.text = "3"
binding.tvCartCount.announceForAccessibility("购物车中有3件商品")
你想想看,如果没有这行代码,用户点了一下「加入购物车」,页面数字变了,但TalkBack没任何反馈。用户会以为操作没成功,又点了一次——结果加了两次。
SVG:无障碍知识体系全景图
实战:给一个登录页面做无障碍改造
光说不练假把式。我们拿一个常见的登录页面来实操。
改造前:
<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或者焦点顺序问题。久而久之,团队里就形成了习惯,新功能上线前都会过一遍无障碍检查。
好了,这一章的内容就到这里。记住,无障碍不是终点,而是起点。你的用户里,总有人需要这份「被看见」的尊重。