一、无障碍开发原理与实践
android无障碍开发是帮忙视障人士也能便捷地使用APP。Android本身提供了标准的无障碍服务,本文主要是搜索模块在开发无障碍服务的一些实践总结。大部分手机系统已支持无障碍,首先用户需要开启无障碍,选择Android设备 “设置->无障碍”,打开TalkBack开关。
浏览方式,开启TalkBack后,可以通过触摸功能浏览屏幕,也可以线性方式滑动浏览屏幕上的各项内容。
比如RecyclerView控件自带手指
线性导航模式要按照一次查看一项内容的方式浏览屏幕,请用一根手指向左或向右滑动,以便按顺序浏览各项内容。当您聚焦于某个项目时,TalkBack 会短暂延迟,随后提示您可以针对该项目执行哪些操作。
二、android无障碍常见用法说明
如果想要使用无障碍,首先需要开启无障碍服务,打开TalkBack服务
在手机中设置。
Android有一部分控件本身支持无障碍,具备获取无障碍焦点,朗读控件角色和控件状态,比如:TextView、Button、ImageView、RecyclerView和CheckBox控件本身支持无障碍朗读。还有一些控件,比如自定义View,LinearLayout,RelativeLayout和FrameLayout等控件本身无法获取无障碍焦点,也不朗读控件角色和控件状态。
一般情况,可以通过添加描述性文字设置无障碍朗读文本。
1. 在布局xml中,给控件添加属性android:contentDescription;2、通过代码调用setContentDescription方法;
如果一个控件本身不支持获取无障碍焦点,可以通过两种方式设置获取无障碍焦点。
1. 在布局xml中添加android:focusable="true"或android:focusableInTouchMode="true",经过本人验证,两个属性添加一个即可。
2. 调用View的setFocusable或setFocusableInTouchMode方法,入参传true。
三、实际开发场景
接下来,通过几种场景描述了无障碍的应用,有实例代码可以直接使用。
实践一:如果想屏蔽无障碍朗读:可以通过以下代码解决
以搜索模块为例:搜索结果页在打开时自动朗读全部内容,这个并不是最好的效果,可以给容器屏蔽无障碍,当容器部子View获取到焦点后朗读。
ViewCompat.setImportantForAccessibility(view,ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
说明:view:入参,传入需要屏蔽无障碍的控件。
在布局文件对应节点下添加属性:android:importantForAccessibility="no"
在java代码中添加以下代码:
分为两类控件:1)本身具备获取无障碍焦点和2)本身不具备获取无障碍焦点的控件。
本身具备获取无障碍焦点的控件有:TextView,Button等
本身不具备获取无障碍焦点的控件有:LinearLayout,RelativeLayout和View等。
所以,如果想要这些控件获取无障碍焦点,可以通过两种方式:
(1)在布局文件中添加属性的方法,比如android:focusable="true",或android:focusableInTouchMode="true"经过自测,这两个属性添加其中任何一个都可以。
(2)也可以通过java代码的方式调用相应控件的setFocusable(true)和setFocusableInTouchMode(true)方法。
实践二:TextView控件标签朗读支持text和contentDescription两种
对于TextView控件来说,无障碍会朗读布局文件这两个属性配置的文案内容,只需二选一设置即可,在xml中添加属性:android:text或android:contentDescription,除了可通过在xml中设置外,还可通过代码调用setText和setContentDescription方法添加无障碍支持。
实践三:本身自带控件角色和控件状态朗读,只需添加无障碍标签即可
RecyclerView是一个列表控件,自带朗读角色和状态,“**选项,在列表中”,CheckBox是一个复选框,本身自带角色和状态朗读,本身会朗读“ 已选中,某某 复选框 ”,只需要添加标签,可实现无障碍支持。
实践四:控件获取无障碍焦点
在一些场景下,比如选中列表某一项,然后刷新整个列表。刚才选中的列表项失去无障碍焦点,所以在整个列表刷新后,需要手动添加获取无障碍焦点。以京东App搜索结果页的筛选面板为例,筛选面板中部分筛选项是通过列表实现的,比如品牌,选中某一个品牌时,刷新整个列表项更新选中态,列表刷新后无障碍焦点丢失,这时就需要调用无障碍获取焦点的方法。示例代码如下:
调用视图对象的sendAccessibilityEvent方法,传入获取焦点事件类型(TYPE_VIEW_HOVER_ENTER)
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
实践五:如何让普通视图选中时,自动朗读角色状态呢?
对于无法自动朗读视图状态的控件,让控件在无障碍模式下朗读角色状态的方法,可以通过给视图添加无障碍代理实现。无障碍代理对象调用setSelected方法设置选中状态,选中状态设置成视图的选中态的值。控件选中时,调用对象的setSelected方法传入true,不能将“已选中”文案写在标签中。
public static void handleAccessibilityOfViewSelected(View view, final String text){
if(view == null){
return;
}
ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setSelected(host.isSelected());
info.setContentDescription(text);
}
});
}
实践六:普通视图选中和CheckBox一样,朗读"已选中,复选框”或者“未选中,复选框"
CheckBox控件,自带的无障碍朗读,选中状态朗读“已选中,** 复选框”,未选中朗读“未选中,**复选框”。
如果一个控件本身不具备无障碍朗读效果,可通过设置一个无障碍代理,包装成CheckBox,这样普通View具备了CheckBox控件的无障碍特性,可朗读控件角色和状态。实例代码如下:
public static void handleAccessibilityOfViewChecked(View view) {
if(view == null){
return;
}
ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
` info.setClassName(CheckBox.class.getName());
info.setCheckable(true);
info.setSelected(false);
info.setChecked(host.isSelected());
}
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
event.setChecked(host.isSelected());
}
}
});
}
实践七:朗读“某某控件已停用”
有一些业务场景,不支持用户选择操作。举个例子:比如当商品无货时,不支持用户选择商品,这时候可以将控件设置为不可用状态。方法也特别简单,调用View类的setEnabled方法,参数传false,无障碍朗读:“某某控件已停用”。
实践八:去除无障碍角色朗读
带有某个角色的控件也会带有一些特性,比如RecyclerView控件,此操作要谨慎使用,因为移除控件的角色外,控件本身具有的属性也会消失。
视图的无障碍控件角色与视图设置的无障碍代理中AccessibilityNodeInfo对象设置的类名称有关。
有一些控件类,本身具备无障碍朗读特性,比如:CheckBox控件,设置了文本名称“Android”,未选中时朗读“ 未选中,Android,复选框”,已选中朗读 “已选中,Android 复选框 ”。如果想屏蔽角色朗读,可以通过给无障碍代理对象设置成不能朗读角色的控件,也就是调用AccessibilityNodeInfo对象的setClassName方法,传入空或其他不支持朗读角色的控件类名称。屏蔽之后朗读“未选中,Android”,选中之后朗读“已选中,Android”。
这个方法在使用上要谨慎,因为角色更改,除了不能朗读控件角色外,控件自带的一些属性也会消失。举个例子:比如一个横向RecyclerView列表,一屏展示不下,可通过无障碍扫动模式自动翻到下一页,实现扫动方式浏览全部列表项。去除无障碍角色的RecyclerView,无法通过扫动方式实现翻页朗读的效果。所以在使用上,需根据业务场景选择是否有必要屏蔽,并且需要多加测试。
public static void handleAccessibilityOfViewClassName(View... views) {
for (View view : views) {
if (view == null) {
return;
}
view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName("");
}
});
}
}
实践九:判断无障碍服务是否开启
有一些业务场景,需要我们判断当前是否开启无障碍,这个方法比较耗时,需要放在子线程。
/**
* 判断版本号
*/
public static boolean buildVersionHeightJellyBean() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
/**
* 判断是否开启了无障碍服务
*/
public static boolean isEnableAccessibilityService(Context context) {
if (!buildVersionHeightJellyBean()) {
return false;
}
AccessibilityManager manager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
if (manager == null || !manager.isEnabled()) {
return false;
}
List<AccessibilityServiceInfo> infos = manager.getEnabledAccessibilityServiceList(1);
if (infos == null || infos.isEmpty() || !manager.isTouchExplorationEnabled()) {
return false;
}
return true;
}
实践十:如何让父容器可以选中,并且朗读
有一些自定义View,本身无法获取焦点,也不具备无障碍朗读功能。比如LinearLayout,RelativeLayout本身没有焦点。可通过在布局文件中添加android:focusable="true"属性和android:focusableInTouchMode="true"属性设置控件可获取焦点。也可以通过代码的方式设置,调用View的setFocusable和setFocusableInTouchMode方法,传入true。这样选中父容器时,可以朗读子控件的标签内容。
需要注意的是,对于可获取焦点的子控件,如果属性没有设置成不可获取焦点,只有控件获取焦点才朗读标签内容,选择它的容器控件时,不会朗读标签内容。
实践十一:自动循环播放的轮播图控件
针对自动循环播放的轮播图,当标签还没朗读完,翻到下一页,开始朗读下一页的标签,这种操作不是很友好。
所以针对这种控件,需首先判断无障碍是否是开启状态,开启状态,关闭循环播放,让用户可以用手指滑动,滑动到下一个图片,朗读标签。
对于无障碍开发,不仅要掌握一定的无障碍开发技能,还要注重实现效果对无障碍人士易用性和友好性。
实践十二:DrawerLayout控件查看源码发现:DrawerLayout类updateChildrenImportantForAccessibility方法,源码如下:
private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if ((!isDrawerOpen && !isDrawerView(child)) || (isDrawerOpen && child == drawerView)) {
// Drawer is closed and this is a content view or this is an
// open drawer view, so it should be visible.
ViewCompat.setImportantForAccessibility(child,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
} else {
ViewCompat.setImportantForAccessibility(child,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
}
}
当我们使用DrawerLayout控件实现抽屉时,抽屉打开时,如果视图是抽屉支持无障碍朗读,如果视图不是抽屉则无障碍不可用。相反,抽屉关闭时,如果视图不是抽屉支持无障碍朗读,如果视图是抽屉则无障碍不可用。
设置无障碍的方法在三处调用,dispatchOnDrawerClosed,dispatchOnDrawerOpened和openDrawer三个方法中调用updateChildrenImportantForAccessibility更新无障碍可用和不可用状态。
对于setDrawerListener方法,传参数DrawerListener,DrawerListener有许多方法,其中最重要的两个方法是抽屉打开与抽屉关闭。
(1)抽屉打开的方法:void onDrawerOpened(@NonNull View drawerView);
以抽屉打开为例:方法调用关系如下:
updateDrawerState>dispatchOnDrawerOpened>onDrawerOpened,那么updateDrawerState在哪里调用呢?
updateDrawerState有3处调用,分别是:
打开抽屉 public void openDrawer(@NonNull View drawerView, boolean animate)
关闭抽屉 public void closeDrawer(@NonNull View drawerView, boolean animate)
public void onViewDragStateChanged(int state)
也就是说无论抽屉打开,关闭,状态变化都会去调用方法,更新无障碍可用状态。
(2)抽屉关闭的方法:void onDrawerClosed(@NonNull View drawerView);
void onDrawerOpened(@NonNull View drawerView);
void onDrawerClosed(@NonNull View drawerView);