Android无障碍宝典

Android江湖上一直流传着一部秘籍——Android无障碍宝典。传闻练成这部宝典,可在Android无障碍模式下,飞檐走壁,能人所不能。宝典分为三篇,分别是入门、进阶和高级,由浅入深,全面展示无障碍的基本方法及扩展应用。

Android应用无障碍化,目的是为视觉障碍或其他有障碍的用户提供更好的服务。在无障碍模式下,用户的操作方式与平常不同,比如:

  • 选择(Hover)一个元素:单击
  • 点击(Click)一个元素:双击
  • 滚动:双指往上、下、左、右
  • 选择上或下一个项目:单指往上、下、左、右
  • 快速回到主画面:单指上滑+左滑
  • 返回键:单指下滑+左滑
  • 最近画面键:单指左滑+上滑
  • 通知栏:单指右滑+下滑

此外,还需要理解在无障碍模式下“无障碍焦点”这个概念。如图1所示,界面上以绿色方框来表示目前获得无障碍焦点的View。拥有无障碍焦点的View,会被TalkBack服务识别,TalkBack会从View中取出相关的无障碍内容,然后提示给用户。

Android无障碍宝典

图1 无障碍焦点

有了对无障碍模式初步的了解,就可以正式开始学习如何为应用无障碍化。

入门篇

为View添加ContentDescription

UI上的可操作元素都应该添加上ContentDescription, 当此元素获得无障碍焦点时,TalkBack服务就取出View的提示语(contentDescription),并朗读出来。

添加ContentDescription有两种方法,第一种是通过在XML布局中设置android:contentDescripton属性,如:

<button android:id="”@+id/pause_button”" android:src="”@drawable/pause”" android:contentdescription="”@string/pause”/"><tton>

但是很多情况下,View的内容描述会根据不同情景需要而改变,比如CheckBox按钮是否被选中,以及ListView中item的内容描述等。这种则需要在代码中使用setContentDescription方法,如:

String contentDescription = "已选中 " + strValues[position];

label.setContentDescription(contentDescription);

设置无障碍焦点

UI上的元素,有的默认带有无障碍焦点,如Button、CheckBox等标准控件,有的如果不设置contentDescription是默认没有无障碍焦点。在开发应用过程中,还会遇到一些UI元素,是不希望它获取无障碍焦点的。以下方法可以改变元素的无障碍焦点:

public void setAccessibilityFocusable(View view, boolean focused){

if(android.os.Build.VERSION.SDK_INT >= 16){

if(focused){

ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);

}else{

ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);

}

}

}

IMPORTANT_FOR_ACCESSIBILITY_YES表示这个元素应该有无障碍焦点,会被TalkBack服务读出描述内容;IMPORTANT_FOR_ACCESSIBILITY_NO表示屏蔽元素的无障碍焦点,手指滑动遍历及触摸此元素,都不会获得无障碍焦点,TalkBack服务也不会读出其描述内容。

发出无障碍事件

IMPORTANT_FOR_ACCESSIBILITY_YES表示这个元素应该有无障碍焦点,会被TalkBack服务读出描述内容;IMPORTANT_FOR_ACCESSIBILITY_NO表示屏蔽元素的无障碍焦点,手指滑动遍历及触摸此元素,都不会获得无障碍焦点,TalkBack服务也不会读出其描述内容。

view.postDelayed(new Runnable() {

@Override

public void run() {

if(android.os.Build.VERSION.SDK_INT >= 14){

view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);

}

}

},100);

这个方法是让View来自动发出被单击选中的无障碍事件,发出后,UI上的无障碍焦点则会马上赋给这个View,从而达到抢无障碍焦点的效果。再比如:

if(android.os.Build.VERSION.SDK_INT >= 16){

AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);

event.setPackageName(view.getContext().getPackageName());

event.setClassName(view.getClass().getName());

event.setSource(view);

event.getText().add(desc);

view.getParent().requestSendAccessibilityEvent(view, event);

}

AccessibilityEvent.TYPE_ANNOUNCEMENT是代表元素需要TalkBack服务来读出描述内容。其中desc是描述内容,将它放到event的getText()中,然后请求View的父类来发出事件。

进阶篇

介绍AccessibilityDelegate

Android中View含有AccessibilityDelegate这个子类,它可被注册进View中,主要作用是为了增强对无障碍化的支持。

查看View的源码可发现,注册Accessibility Delegate方法很简单:

Public void setAccessibilityDelegate(AccessibilityDelegate delegate) {

mAccessibilityDelegate = delegate;

}

注册后,View对无障碍的处理,则会交给AccessibilityDelegate,如:

public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {

if (mAccessibilityDelegate != null) {

mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(this, info);

} else {

onInitializeAccessibilityNodeInfoInternal(info);

}

}

onInitializeAccessibilityNodeInfo是View源码中初始化无障碍节点信息的方法,从上面代码看出,当mAccessibilityDelegate是开发注册的AccessiblityDelegate时,则会执行AccessiblityDelegate中的onInitializeAccessibilityNodeInfo方法。再看看AccessibilityDelegate类中的onInitializeAccessibilityNodeInfo:

public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {

host.onInitializeAccessibilityNodeInfoInternal(info);

}

Host是被注册AccessibilityDelegate的View,onInitializeAccessibilityNodeInfoInternal是View中真正初始化无障碍节点信息的方法。即是说,注册了AccessibilityDelegate并没有改变View原来对无障碍的操作,而是在这个操作之后增加了处理。

AccessibilityDelegate的应用

下面介绍下AccessibilityDelegate可以提供哪些无障碍应用,注册AccessibilityDelegate是在API 14以上才开放的接口,API 14以下如需使用,可以接入support v4包中的AccessibilityDelegateCompt。注册方法如下:

if (Build.VERSION.SDK_INT >= 14) {

View view = findViewById(R.id.view_id);

view.setAccessibilityDelegate(new AccessibilityDelegate() {

public void onInitializeAccessibilityNodeInfo(View host,

AccessibilityNodeInfo info) {

super.onInitializeAccessibilityNodeInfo(host, info);

// 对info做出扩展性支持

});

}

要对info做出扩展支持,还得先了解AccessibilityNodeInfo这个类。Android开发都知道,UI上的元素是通过View来实现,而AccessibilityNodeInfo则是存储View的无障碍信息(如contentDescription)及无障碍状态(如focusable、visiable、clickable等),同时它还肩负着TalkBack服务和View之间通讯的桥梁作用。如果要修改View的无障碍提示,比如修改View的类型提示,可以这样做:

if(android.os.Build.VERSION.SDK_INT >= 14){

view.setAccessibilityDelegate(new AccessibilityDelegate(){

@Override

public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {

super.onInitializeAccessibilityNodeInfo(host, info);

if(contentDesc != null) {

info.setContentDescription(contentDesc);

}

info.setClassName(className);

}

});

}

className是类型名称,这里如果是Button.class.getName(),则TalkBack会对这个View读“XXX 按钮”,“XXX”是contentDescription,而“按钮”则是TalkBack服务添加的(如果是英文环境,则是“XXX button”)。使用上面的方法,可以为非按钮控件加上“按钮”的提示,方便无障碍用户识别UI上元素的作用,同时又不必把“按钮”提示强加入contentDescription中。除了修改AccessibilityNodeInfo外,使用AccessibilityDelegate还可以影响无障碍事件,如:

if (android.os.Build.VERSION.SDK_INT >= 14) {

view.setAccessibilityDelegate(new AccessibilityDelegate() {

@Override

public void sendAccessibilityEvent(View host, int eventType) {

// 弹出Popup后,不自动读各项内容

if (eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {

super.sendAccessibilityEvent(host, eventType);

}

}

});

}

无障碍模式下,弹出Dialog,则会把Dialog中的所有元素都读一遍。这个方法可以把弹起窗口的无障碍事件拦截,Dialog弹起就不会再自动读各项内容。

高级篇

有了AccessibilityDelegate这把利器之后,开发可以轻松对应Android中大部分的无障碍化,但是如果想要做到游刃有余,还得深造更高级的功夫——自定义View无障碍化。

应用开发过程中,总会需要自定义View来实现特殊的UI效果,当一个自定义View中包含多种UI元素时,无障碍模式下并不能区分包含的多种UI元素,而只为自定义View添加一个大无障碍焦点。如图2所示。

Android无障碍宝典

图2 自定义View只有一个大无障碍焦点

图中只有一个大无障碍焦点,因为这是一个View,里面的文字及蓝色的矩形都是绘制出来的。

@Override

public void onDraw(Canvas c) {

super.onDraw(c);

if (mTitle != null) {

drawTitle(c);

}

for (int i = 0; i < mSize; i++) {

drawBarAtIndex(c, i);

}

drawAxisY(c);

}

代码中绘制出来的元素不可被TalkBack识别出来,所以开发需要多做一步,对自定义View无障碍化。这里将介绍如何通过官方提供的ExploreByTouchHelper来实现。

Android无障碍宝典

图3 自定义View内元素获得无障碍焦点

图3是使用ExploreByTouchHelper实现的最终效果,每一个小矩形都能获取到无障碍焦点,且可以进行选中高亮。实现ExploreByTouchHelper需要五步。

第一步,委托处理无障碍。

public class BarGraphView extends View {

private final BarGraphAccessHelper mBarGraphAccessHelper;

public BarGraphView(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

...

mBarGraphAccessHelper = new BarGraphAccessHelper(this);

ViewCompat.setAccessibilityDelegate(this, mBarGraphAccessHelper);

}

@Override

public boolean dispatchHoverEvent(MotionEvent event) {

if ((mBarGraphAccessHelper != null)

&& mBarGraphAccessHelper.dispatchHoverEvent(event)) {

return true;

}

return super.dispatchHoverEvent(event);

}

}

mBarGraphAccessHelper继承ExploreBy TouchHelper,可通过注册AccessibilityDelegate的方式来注册给自定义BarGraphView,同时让mBarGraphAccessHelper来处理Hover事件(无障碍模式下的点击)的分发。

第二步,标记无障碍虚拟节点ID。

private class BarGraphAccessHelper extends ExploreByTouchHelper {

private final Rect mTempParentBounds = new Rect();

public BarGraphAccessHelper(View parentView) {

super(parentView);

}

@Override

protected int getVirtualViewIdAt(float x, float y) {

final int index = getBarIndexAt(x, y);

if (index >= 0) {

return index;

}

return ExploreByTouchHelper.INVALID_ID;

}

@Override

protected void getVisibleVirtualViewIds(List<integer> virtualViewIds) {

final int count = getBarCount();

for (int index = 0; index < count; index++) {

virtualViewIds.add(index);

}

}

}

getVirtualViewIdAt和getVisibleVirtualViewIds都是ExploreByTouchHelper类需要实现的方法,分别代表获取虚拟无障碍节点的id以及设置虚拟无障碍节点的id。由于自定义View里的元素非继承于View,如要在无障碍模式下被识别,则需要构造一个虚拟无障碍节点。构造方法已封装到ExploreByTouchHelper里,开发只需要告诉ExploreByTouchHelper有哪些虚拟无障碍节点的id即可。无障碍节点id需要满足以下条件:id是一个接一个的,稳定且为非负整数。设置好无障碍虚拟节点id后,根据用户操作UI上的xy坐标,取得对应的无障碍虚拟节点id,通过getVirtualViewIdAt方法告诉ExploreByTouchHelper类。

第三步,填充无障碍节点的属性。

private class BarGraphAccessHelper extends ExploreByTouchHelper {

...

private CharSequence getDescriptionForIndex(int index) {

final int value = getBarValue(index);

final int templateRes = ((mHighlightedIndex == index) ?

R.string.bar_desc_highlight : R.string.bar_desc);

return getContext().getString(templateRes, index, value);

}

@Override

protected void populateEventForVirtualViewId(int virtualViewId, AccessibilityEvent event) {

final CharSequence desc = getDescriptionForIndex(virtualViewId);

event.setContentDescription(desc);

}

@Override

protected void populateNodeForVirtualViewId(

int virtualViewId, AccessibilityNodeInfoCompat node) {

final CharSequence desc = getDescriptionForIndex(virtualViewId);

node.setContentDescription(desc);

node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);

final Rect bounds = getBoundsForIndex(virtualViewId, mTempParentBounds);

node.setBoundsInParent(bounds);

}

}

构造了虚拟无障碍节点后,便可以往节点里塞无障碍信息。populateEventForVirtualViewId是将无障碍信息填入无障碍事件中。populateNodeForVirtualViewId是初始化每个虚拟无障碍节点,设置contentDescription,注册所需要处理的Action,以及设置无障碍焦点的边框。setBoundsInParent一定要设置有效的边框,否则会导致虚拟无障碍节点无法获取无障碍焦点。

第四步,提供用户无障碍交互支持。

private class BarGraphAccessHelper extends ExploreByTouchHelper {

...

@Override

protected boolean performActionForVirtualViewId(

int virtualViewId, int action, Bundle arguments) {

switch (action) {

case AccessibilityNodeInfoCompat.ACTION_CLICK:

onBarClicked(virtualViewId);

return true;

}

return false;

}

}

private void onBarClicked(int index) {

setSelection(index);

if (mBarGraphAccessHelper != null) {

mBarGraphAccessHelper.sendEventForVirtualViewId(

index, AccessibilityEvent.TYPE_VIEW_CLICKED);

}

}

小矩形点击后是会被选中且高亮的,在performActionForVirtualViewId中实现对应的点击事件处理。经过这四步,自定义View就可以完美支持无障碍化了!

可以看出,ExploreByTouchHelper简化了虚拟节点层次结构的构造,封装AccessibilityNodeProvider的实现,更完善的控制Hover事件、无障碍事件。有了它,Android无障碍化再也不是难题。

Android无障碍化宝典的内容就介绍到此,在实际开发中,遇到的无障碍化问题都比较细小和琐碎,希望以上介绍能提供一点帮助。很多Android开发以为无障碍化就是为控件加上ContentDescription,其实还有空描述、混乱焦点、焦点顺序、描述准确性等地方需要注意和优化。只有用心、持续地改进和优化,才能做出真正无障碍的产品。

相关推荐