26、无障碍开发:TalkBack支持、内容描述与焦点顺序优化
说实话,无障碍开发这个话题,在很多Android开发者的优先级列表里排得挺靠后的。我以前也这么觉得——项目工期紧,功能都做不完,谁有空管这个?直到有一次,我亲眼看到一位视障用户拿着遥控器,对着我做的TV应用干着急,TalkBack读出来的内容完全驴唇不对马嘴。那一刻,说实话,挺扎心的。
从那天起,我就把无障碍当成一个硬性指标来做了。今天咱们就聊聊,在TV和大屏应用里,怎么把TalkBack支持、内容描述和焦点顺序这些事做好。
为什么TV应用的无障碍比手机更重要?
你想想看,手机上的视障用户可以用触屏手势、语音助手来操作。但在TV上,遥控器就那么几个按键。如果TalkBack读不出来正确的信息,用户基本就等于被挡在门外了。
我个人的习惯是:把无障碍当成功能的一部分来开发,而不是最后补丁。这样反而省时间,因为不用回头改布局。
TalkBack支持:让遥控器“说话”
TalkBack在TV上的工作方式和手机不太一样。手机靠触摸,TV靠焦点移动。当某个View获得焦点时,TalkBack就会读出它的内容。
最基本的支持,就是给所有可交互的控件设置contentDescription:
<Button
android:id="@+id/btn_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="播放当前视频" />
嗯,这里要注意:不要用控件本身的文本作为描述。比如按钮上写着“播放”,但TalkBack读出来还是“播放”,这就浪费了一次信息传递的机会。更好的做法是加上上下文:
android:contentDescription="播放《流浪地球2》"
核心原则:contentDescription 应该告诉用户“这是什么”以及“操作后会怎样”。
内容描述:别让用户猜谜
我在项目中遇到过最典型的问题,就是图片没有描述。TV上很多海报、封面图都是ImageView,如果不加描述,TalkBack只会读出一个“图片”或者“未标记”。用户根本不知道焦点落在哪。
正确的做法:
<ImageView
android:id="@+id/img_movie_poster"
android:layout_width="200dp"
android:layout_height="300dp"
android:contentDescription="电影《肖申克的救赎》海报,评分9.7" />
对于纯装饰性的图片,比如分割线、背景纹理,应该设置:
android:contentDescription="@null"
android:importantForAccessibility="no"
这样TalkBack就会跳过它,不会干扰用户。
焦点顺序优化:别让用户乱跳
TV应用靠遥控器方向键移动焦点。如果焦点顺序乱了,用户按一下“下”,焦点却跳到右边去了,体验极差。
我见过最离谱的一个项目,焦点在九宫格布局里乱跳,用户想从第一行移到第二行,结果焦点直接飞到右下角去了。嗯,这种问题通常是因为系统自动计算的焦点顺序不符合预期。
解决办法有两个:
- 显式指定焦点顺序:使用
android:nextFocusDown、android:nextFocusUp等属性。 - 调整布局层级:让系统自动计算时更合理。
我个人更推荐第一种,因为可控性高:
<Button
android:id="@+id/btn_1"
android:nextFocusDown="@id/btn_4"
android:nextFocusRight="@id/btn_2" />
<Button
android:id="@+id/btn_2"
android:nextFocusDown="@id/btn_5"
android:nextFocusLeft="@id/btn_1" />
小技巧:在RecyclerView或GridLayout中,可以用android:focusableInTouchMode="true"来确保每个item都能获得焦点。但别滥用,否则焦点会变得很“粘”。
知识体系结构图
下面这张图,是我自己整理的无障碍开发核心逻辑。你看一眼就能明白各个模块之间的关系:
避坑指南:我曾经踩过的几个坑
坑一:自定义View不发声
我曾经写了一个自定义的播放器控件,里面用Canvas画了各种按钮。结果TalkBack完全不识别。后来才发现,自定义View需要手动处理onInitializeAccessibilityNodeInfo和onPopulateAccessibilityEvent。说白了,系统不知道你画了个啥,你得告诉它。
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
super.onInitializeAccessibilityNodeInfo(info)
info?.contentDescription = "播放控制面板,包含播放、暂停、快进按钮"
info?.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)
}
坑二:焦点被“吃掉”了
有一次,用户反馈说遥控器按了没反应。查了半天,发现是一个FrameLayout设置了android:focusable="true",但它本身没有任何交互逻辑。焦点落到它上面就卡住了,再也移不出来。解决办法很简单:只有真正需要交互的控件才设置focusable。
警告:不要给容器布局随意设置 focusable="true"。除非你确定它需要独立接收焦点事件。
实战:一个完整的例子
假设我们有一个视频列表页面,每行显示三部电影。用户通过方向键左右选择,按确定键进入详情。代码可以这样写:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/movie_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="电影列表,共20部电影"
android:focusable="true" />
<!-- 每个item的布局 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:contentDescription="电影《{{title}}》,评分{{rating}}"
android:nextFocusLeft="@id/prev_item"
android:nextFocusRight="@id/next_item">
<ImageView
android:id="@+id/movie_poster"
android:layout_width="120dp"
android:layout_height="180dp"
android:contentDescription="{{title}}海报"
android:importantForAccessibility="yes" />
<TextView
android:id="@+id/movie_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="{{title}}"
android:contentDescription="电影名称:{{title}}" />
</LinearLayout>
这里有个细节:RecyclerView本身也要设置contentDescription,这样当焦点进入列表时,TalkBack会先读一句“电影列表,共20部电影”,让用户知道当前在哪个区域。
测试:别靠猜,用真机
我个人的习惯是,每次提交代码前,都会打开TalkBack(设置 → 无障碍 → TalkBack),然后用遥控器把整个页面走一遍。你很快就会发现哪些地方描述不对、哪些焦点跳得不合理。
如果条件允许,找一位真实用户测一下。你会发现很多自己完全想不到的问题。比如,有些用户习惯把遥控器拿得很近,有些则离得很远——这会影响他们看屏幕上的焦点高亮。嗯,这些细节,只有真实用户才能告诉你。
测试清单:
- 所有可点击的控件都有contentDescription
- 装饰性元素被标记为不重要
- 焦点顺序符合视觉布局(从左到右,从上到下)
- 自定义View正确处理无障碍事件
- 列表/网格的容器也有描述
好了,关于无障碍开发,今天就聊到这儿。说白了,就是多花一点心思,让更多人能用你的应用。这个投入,绝对值。
公众号:蓝海资料掘金营,微信deep3321