让应用无障碍

当视觉、肢残和老龄化用户激活设备中的无障碍服务和特性时,Android应用将会更加无障碍。无障碍服务让应用更加无障碍,即使在代码中不做任何的无障碍修改。但是,需要采取以下步骤来评估应用的无障碍性能,确保所有用户都有愉快的使用体验。

保证所有用户的无障碍体验需要做到以下几步,特别是使用Android框架提供的组件创建用户界面时。如果应用只使用标准组件,步骤如下:

  1. 使用android:contentDescription属性为应用中的交互控件添加描述性文本,特别是ImageButtonImageViewCheckBox
  2. 保证所有可以接收输入(触摸和输入)的用户界面元素都可以使用定向控制到达,例如:轨迹球、D-pad(物理或虚拟)、导航手势
  3. 保证在音频反馈的同时伴有视觉反馈或通知,帮助听障用户使用应用。
  4. 使用无障碍导航服务和特性测试应用。打开TalkBack触摸浏览(Explore by Touch),然后尝试使用定向控制操作应用。更多无障碍测试信息,详见无障碍测试清单( Accessibility Testing Checklist)。

如果自定义控件继承了View类,需要另外的工作来保证组件无障碍。该章节讨论的是怎样让自定义控件拥有无障碍特性。

注意:该章节的实现步骤描述了为视障用户对应用进行无障碍改造的需求。在无障碍开发清单(Accessibility Developer Checklist)中需要复查视障用户的无障碍需求。

标记用户界面元素

很多用户界面控件依靠视觉提示告知用户含义和作用。例如:一个记事本程序可能会使用一个带有加号图片的ImageButton来提示用户可以通过它添加一个新的记录。或者,一个EditText元素的附近可能会有一个标签来提示目的。视障用户不能清晰看到这些提示并遵循它们,导致这些提示无效。

可以使用android:contentDescription XML布局属性来让这些控件更加无障碍。该属性中的文本不会出现在屏幕上,但如果用户启用了可以提供音频反馈的无障碍服务,当用户导航到这些控件的时候,描述文本将会被读出。

因此,在应用的用户界面中需要为每个ImageButtonImageViewCheckBox控件设置android:contentDescription属性,同时也需要为视障用户在那些可能需要额外信息的输入控件中添加描述文本。

例如,下面的ImageButton使用add_note字符串资源为加号按钮设置内容描述,该资源在英文界面可能被定义为“Add note”:

<ImageButton
    android:id=”@+id/add_note_button”
    android:src=”@drawable/add_note”
    android:contentDescription=”@string/add_note”/>

通过给图像按钮添加描述文本,当用户将焦点移到该按钮,或者鼠标悬停时,提供语音反馈的无障碍服务可以读出“Add note”。

注意:对于EditText区域,提供android:hint属性代替内容描述,文本区域为空的时候此属性帮助用户理解应该输入什么样的内容。当文本区域填充上内容,TalkBack将会读出输入的文本,而不会读出提示文本。

启用焦点导航

焦点导航允许残障用户使用定向控制器一步步的浏览用户界面控件。定向控制器可以是物理的,如轨迹球、定向垫(D-pad)、方向键,或虚拟的,如非视觉键盘( Eyes-Free Keyboard)、和Android4.1以及以上可用的手势导航。定向控制器是很多Android用户导航的首选方式。

为了保证用户可以使用定向控制器导航应用,需要验证应用中所有用户界面输入控件,在不使用触屏的情况下可以到达和激活。应该验证点击定向控制器的中心按钮(或OK按钮)和触屏的触摸操作有同样的效果。更多关于测试定向控制的信息,详见测试焦点导航

启用视图焦点

当用户界面元素的android:focusable属性设置为true时,可以使用定向控制到达该元素。该设置允许用户使用定向控制聚焦元素并与之交互。 Android框架提供的用户界面控件默认可聚焦,并通过改变控件的外观可直观表明其聚焦。

Android提供了几个API让开发者决定用户界面控件是否可聚焦,甚至请求给控件赋予焦点:

如果视图不是默认聚焦,可以在布局文件中设置android:focusable属性为true,或者调用setFocusable()方法让视图可聚焦。

控制焦点顺序

当用户在任何方向使用定向控制器导航时,焦点从一个用户界面元素(视图)传递到另一个由焦点顺序指定的用户界面元素(视图)。焦点顺序是以一种在某一特定方向上寻找相邻元素的算法为基础的。在极少数情况下,默认的算法可能不匹配开发者定义的顺序,或可能对于用户不符合逻辑。在这些情况下,可以在布局文件中使用下列的xml属性明确地覆盖焦点顺序:

android:nextFocusDown
当用户向下导航时,定义下一个接收焦点的视图;
android:nextFocusLeft
当用户向左导航时,定义下一个接收焦点的视图;
android:nextFocusRight
当用户向右导航时,定义下一个接收焦点的视图;
android:nextFocusUp
当用户向上导航时,定义下一个接收焦点的视图。

下面的XML布局示例中展示了两个可聚焦的用户界面元素,这两个元素的android:nextFocusDownandroid:nextFocusUp属性被明确地设置。TextView位于EditText的右边。但是,因为设置了这些属性,当EditText聚焦时,按下下光标可以将焦点移到TextView元素上。

<LinearLayout android:orientation="horizontal"
        ... >
    <EditText android:id="@+id/edit"
        android:nextFocusDown=”@+id/text”
        ... />
    <TextView android:id="@+id/text"
        android:focusable=”true”
        android:text="Hello, I am a focusable TextView"
        android:nextFocusUp=”@id/edit”
        ... />
</LinearLayout>

当修改焦点顺序时,确保每一个用户界面控件的所有方向的导航能按照预期工作,尤其是反向导航的时候(从来的路径返回)。

注意:开发者可以使用诸如setNextFocusDownId()setNextFocusRightId()的方法,在程序运行的时候修改用户界面组件的聚焦顺序。

创建可访问自定义视图

如果应用需要一个自定义视图组件,开发者必须做一些额外的工作来确保自定义视图是可访问的。这些都是确保视图可访问性的主要任务:

处理定向控制器的点击

在大多数的Android设备上,使用定向控制器单击视图会发送一个带有KEYCODE_DPAD_CENTERKeyEvent事件到当前的焦点视图。所有的标准Android视图已经适当地处理KEYCODE_DPAD_CENTER。当构建一个自定义View控件,确保这个事件的效果跟在触摸屏上触摸视图的效果一样。

自定义控件也应该将KEYCODE_ENTER事件作为KEYCODE_DPAD_CENTER事件处理,这种方法使全键盘用户的交互更加容易。

实现无障碍API方法

无障碍事件是用户在应用中与视觉界面元素的交互消息。这些消息是由无障碍服务(Accessibility Services)处理的,这些服务使用这些事件中的信息去产生补充反馈和提示。在Android4.0(API级别14)和更高的系统中,生成无障碍事件的方法被扩展到比Android1.6(API级别4)的AccessibilityEventSource提供更多的详细信息。扩展无障碍方法是View类和View.AccessibilityDelegate类的一部分。这些方法如下:

sendAccessibilityEvent()
(API级别4)当用户操作视图时,这个方法被调用。该事件使用用户操作类型进行分类,例如TYPE_VIEW_CLICKED。开发者一般不需要实现这个方法,除非创建了自定义视图。
sendAccessibilityEventUnchecked()
(API级别4)当调用的代码需要直接检查设备是否激活无障碍特性(AccessibilityManager.isEnabled())的时候,调用该方法。如果实现该方法,必须执行无障碍激活的检查,而不管系统真正的设置。一般的,不需要为自定义视图实现该方法。
dispatchPopulateAccessibilityEvent()
(API级别4)当自定义视图产生无障碍事件时,系统调用这个方法。在API级别14中,该方法的默认实现是为视图调用onPopulateAccessibilityEvent(),然后为该视图的子元素调用 dispatchPopulateAccessibilityEvent()。为了支持Android4.0(API级别14)之前的版本,开发者必须重写该方法,使用自定义视图的描述性文本填充getText(),描述性文本将会被无障碍服务读出,例如TalkBack。
onPopulateAccessibilityEvent()
(API级别14)该方法为视图的AccessibilityEvent设置朗读文本提示。如果视图是另一个生成无障碍事件视图的子视图,调用该方法。

注意: 通过该方法修改文本以外的属性可能会被其他方法重写。虽然在该方法中开发者可以修改无障碍事件的属性,但是应该做到这些改变仅限于文本内容,并使用onInitializeAccessibilityEvent()方法修改事件的其他属性。

注意: 如果该事件的实现完全重写了输出文本,而不允许布局的其他部分修改这个内容,不要在代码中调用该方法的父类实现。

onInitializeAccessibilityEvent()
(API级别14)系统调用此方法来获取超出文本内容的视图状态的附加信息。如果自定义视图提供简单TextViewButton以外的交互控制,开发者应该重写该方法,并且使用该方法设置该视图的额外信息到事件中,如密码区域类型,复选框类型或提供用户交互或反馈的状态。如果重写这个方法,开发者必须调用它父类的实现方法,然后只修改那些父类中尚未设置的属性。
onInitializeAccessibilityNodeInfo()
(API级别14)该方法为无障碍服务提供视图的状态信息。默认View的实现包含一组标准的视图属性,但是如果自定义视图提供了超出TextViewButton的交互,开发者应该重写该方法,并在AccessibilityNodeInfo对象中设置视图的额外信息,该对象被该方法处理。
onRequestSendAccessibilityEvent()
(API级别14)当视图中的一个子视图生成AccessibilityEvent时,系统调用这个方法。该步骤允许父视图使用额外信息修复无障碍事件。当自定义视图有子视图,且其父视图可以为无障碍事件提供对无障碍服务有用的上下文信息的时候,此时开发者应该实现这个方法。

为了在自定义视图中支持这些无障碍方法,应该采取下列方法中的一种:

  • 如果应用程序目标版本是Android4.0(API级别14)或更高,就直接在自定义视图类中重写并实现上面列出的无障碍方法。
  • 如果自定义视图的目的是要兼容Android1.6(API级别4)及以上,在项目中添加版本5或更高的支持库。然后,在自定义视图类中,调用ViewCompat.setAccessibilityDelegate()方法来实现上面的无障碍方法。对于这种方法的示例,请参阅支持库(版本5或更高)AccessibilityDelegateSupportActivity例子在(<sdk>/extras/android/support/v4/samples/Support4Demos/)。

在任何一种情况下,需要为自定义视图类实现下面的无障碍方法:

更多信息实现这些方法,请参阅填充无障碍事件(Populating Accessibility Events)。

发送无障碍事件

根据自定义视图的特性,不是由默认实现处理的事件,可能需要在不同时刻发送AccessibilityEvent对象。View类为这些事件类型提供了默认的实现方法:

注意:悬停(Hover)事件与触摸浏览相关联,触摸浏览使用这些事件作为触发器,为用户界面元素提供音频反馈。

一般来说,每当自定义视图的内容发生变化时,应该发送一个AccessibilityEvent事件。例如,如果创建了一个自定义的滑动条,用户可以按下左边或右边的箭头选择数值,当滑动条的值发生改变时,自定义视图应该发出一个TYPE_VIEW_TEXT_CHANGED 类型的事件。下面的示例代码演示了使用sendAccessibilityEvent()方法报告该事件:

@Override
public boolean onKeyUp (int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
        mCurrentValue--;
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
        return true;
    }
    ...
}

填充无障碍事件

每个AccessibilityEvent有一组必需的属性,这些属性描述当前视图的状态。这些属性包括视图类名称、内容描述和检查状态等信息。每个事件类型必须的特定属性被描述在AccessibilityEvent参考文档中。View实现提供这些属性的默认值。包含类名和事件时间标记在内的很多值会被自动提供。如果正在创建一个自定义视图组件,必须提供一些关于视图内容和特性的信息。这些信息可能是简单的按钮标签,但是也可能包含想要添加到事件中的额外的状态信息。

一个自定义视图为无障碍服务提供信息的最低要求是实现dispatchPopulateAccessibilityEvent()方法。系统调用这个方法,为AccessibilityEvent请求信息,让自定义视图兼容Android1.6(API级别4)以上的无障碍服务。下面的示例代码展示了该方法的基本实现:

@Override
public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    super.dispatchPopulateAccessibilityEvent(event);
    //在API级别14及以上版本中,调用父类实现将文本填充到事件;
	//在低于API级别14的系统中,检查事件的文本内容,并为自定义视图添加合适的文本描述;
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        event.getText().add(text);
    }
}

在Android 4.0(API级别14)和更高的系统中,使用onPopulateAccessibilityEvent()onInitializeAccessibilityEvent()方法来填充或修改AccessibilityEvent事件中的信息。onPopulateAccessibilityEvent()方法可专门用来为事件添加或修改文本内容,这些信息会被如TalkBack的无障碍服务转化为音频反馈。使用onInitializeAccessibilityEvent()方法添加事件的其他信息,比如视图的选择状态。

此外,还应该实现onInitializeAccessibilityNodeInfo()方法。该方法填充AccessibilityNodeInfo对象,视图层次在接收此事件后生成无障碍事件,无障碍服务使用AccessibilityNodeInfo对象访问该视图层次,获得更多的上下文信息并为用户提供合适的反馈。

下面的示例代码显示了如何使用ViewCompat.setAccessibilityDelegate()重写这三种方法。注意,此示例代码要求添加API级别4(或更高)的Android支持库(Support Library)到项目。

ViewCompat.setAccessibilityDelegate(new AccessibilityDelegateCompat() {
    @Override
    public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(host, event);
		//调用父类实现来填充事件的文本。然后添加父类中不存在的文本。
		//常常只需要在自定义视图中添加这些文本。
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            event.getText().add(text);
        }
    }
    @Override
    public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(host, event);
        //调用父类实现让父类设置事件属性。然后添加父类不支持的新属性。
        event.setChecked(isChecked());
    }
    @Override
    public void onInitializeAccessibilityNodeInfo(View host,
            AccessibilityNodeInfoCompat info) {
        super.onInitializeAccessibilityNodeInfo(host, info);
		//调用父类实现让父类设置适当的info属性。然后添加父类不支持的属性(checkable and checked)。
        info.setCheckable(true);
        info.setChecked(isChecked());
		//经常只需要添加自定义视图的文本。
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            info.setText(text);
        }
    }
}

在Android 4.0(API级别14)和更高的应用程序中,可以在自定义视图类中直接实现这些方法。这种方法的另一个例子,请参阅Android支持库(Support Library)(版本5或更高)的示例,AccessibilityDelegateSupportActivity 样本在 (<sdk>/extras/android/support/v4/samples/Support4Demos/)。

提供自定义无障碍内容

在Android4.0(API级别14)中,Android框架得到改善,可以允许无障碍服务访问可生成无障碍事件的用户界面组件包含的视图层次。此改善允许无障碍服务提供一组更丰富的可以帮助用户的上下文信息。

在某些情况下,无障碍服务不能从视图层次中获得足够的信息。一个自定义界面控件样例包含两个以上的可点击区域,例如一个日历控件。这种情况下,无障碍服务无法获得足够的信息,因为可点击部分不是视图层次的一部分。

图1 可选择日期的自定义日历视图。

在图1所示的例子中,整个日历是作为独立View实现,因此如果不采取其他措施,无障碍服务就不能获得有关视图和视图中用户选择的足够信息。例如,如果用户点击了包含17的日期,无障碍框架只能接收到整个日历控件的描述信息。这种情况下,TalkBack无障碍服务会简单读出“日历”,或者稍好一些的读出“四月日历”,而用户将会不知道自己选择了哪天。

在像这样的情况下,为了给无障碍服务提供足够的上下文信息,Android框架提供了一种方法——指定虚拟视图层次。应用开发者可以使用虚拟视图层次方法为无障碍服务提供一个附加视图层次,该层次能最大限度的接近屏幕上的真实信息。这种方法允许无障碍服务给用户提供更有用的上下文信息。

另一个需要虚拟视图继承结构的情况是,用户界面包含一组功能密切相关的控件(视图),操作一个控件会影响一个或多个组内其他元素的内容,例如增加按钮和减少按钮分开的数值选择器。在这种情况下,无障碍服务无法获得足够的信息,因为一个控件的操作会改变另一个控件的内容,且无障碍服务无法获得这种关系。为了处理这种情况,将包含视图的相关控件进行分组,从该容器中提供虚拟视图继承,清楚呈现控件提供的信息和行为。

为了提供虚拟视图继承,在自定义视图或者视图组中重写getAccessibilityNodeProvider()方法,并返回AccessibilityNodeProvider的实现。该无障碍特性实现的一个例子,详见ApiDemos样例工程中的AccessibilityNodeProviderActivity。在Android1.6和以后的版本中,可以使用Support Library中的ViewCompat.getAccessibilityNodeProvider()方法,并提供AccessibilityNodeProviderCompat实现,就可以兼容虚拟视图继承。

处理自定义触摸事件

自定义视图控件可能需要非标准的触摸事件行为。例如自定义控件可能使用onTouchEvent(MotionEvent)监听方法来检测ACTION_DOWNACTION_UP事件,并触发一个特殊的点击事件。为了保证无障碍服务的兼容性,处理自定义点击事件的代码必须如下:

  1. 为解释点击操作生成适当的AccessibilityEvent
  2. 启用无障碍服务来为不能使用触摸屏的用户执行自定义单击操作。

要有效的处理这些需求,代码应该重写performClick()方法,需要调用该方法的父类实现,然后执行点击事件需要的任何操作。当检测到自定义点击操作时,代码应该调用performClick()。下列代码示例展示了这一模式:

class CustomTouchView extends View {

    public CustomTouchView(Context context) {
        super(context);
    }

    boolean mDownTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        //监听向下向上触摸事件
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (mDownTouch) {
                    mDownTouch = false;
                    performClick(); //调用该方法处理响应,启动无障碍服务为那些无法点击的用户执行操作。
                    return true;
                }
        }
        return false; //为其他触摸事件返回假。
    }

    @Override
    public boolean performClick() {
        //调用父类实现,父类会生成无障碍事件,并且在视图上调用onClick()监听器,如果任何;
        super.performClick();

        //在这里处理自定义点击操作

        return true;
    }
}

上面的代码通过使用performClick()方法保证自定义点击事件与无障碍服务兼容,生成无障碍事件并且为无障碍服务提供入口,并代替用户执行自定义点击事件。

注意:如果自定义视图有不同的可点击区域,例如自定义日历视图,必须在自定义视图中重写getAccessibilityNodeProvider()实现虚拟视图继承(virtual view hierarchy),保证与无障碍服务兼容。

测试无障碍

测试应用的无障碍是保证良好用户体验的重要部分。开发者可以通过在应用程序启用语音反馈和仅使用定向控制在应用程序内导航来测试最重要的无障碍特性。更多无障碍测试信息,详见无障碍测试清单(Accessibility Testing Checklist)