让自定义视图使用起来更没有障碍

如果您的应用需要自定义视图组件,则必须执行一些额外的操作才能让视图使用起来更没有障碍。以下是改进自定义视图无障碍功能的主要任务:

处理方向控制器点击操作

在大多数设备上,使用方向控制器点击视图会向当前获得焦点的视图发送带有 KEYCODE_DPAD_CENTERKeyEvent。所有标准 Android 视图均已妥善处理 KEYCODE_DPAD_CENTER。构建自定义 View 控件时,请确保此事件与轻触触摸屏上的视图具有相同的效果。

您的自定义控件还应将 KEYCODE_ENTER 事件视为与 KEYCODE_DPAD_CENTER 相同。这种方法可让用户更轻松地通过全键盘进行互动。

实现无障碍 API 方法

无障碍事件是有关用户与应用中的可视化界面组件互动的消息。这些消息由无障碍服务处理,后者使用这些事件中的信息生成补充反馈和提示。在 Android 4.0(API 级别 14)及更高版本中,用于生成无障碍事件的方法已进行扩展,提供的信息比 Android 1.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() 方法。为了在早于 4.0(API 级别 14)的 Android 修订版上支持无障碍服务,您必须替换此方法并使用自定义视图的描述性文字填充 getText(),这些文字会由 TalkBack 等无障碍服务读出。
onPopulateAccessibilityEvent()
(API 级别 14)此方法为您的视图设置 AccessibilityEvent 的文字转语音提示。如果该视图是生成无障碍事件的视图的子级,则也调用此方法。

注意:修改此方法中除文字之外的其他属性可能会替换其他方法设置的属性。虽然您可以使用此方法修改无障碍事件的属性,但应将这些更改限制为文字内容,并使用 onInitializeAccessibilityEvent() 方法修改事件的其他属性。

注意:如果此事件的实现会完全替换输出文字且不允许布局的其他部分修改其内容,则请勿在您的代码中调用此方法的超类实现。

onInitializeAccessibilityEvent()
(API 级别 14)除了文字内容之外,系统还会调用此方法来获取有关视图状态的其他信息。如果您的自定义视图提供除了简单的 TextViewButton 之外的其他互动控件,则您应替换此方法并将有关视图的其他信息设置到使用此方法的事件中,如密码字段类型、复选框类型或者提供用户互动或反馈的状态。如果您替换此方法,则必须调用其超类实现,然后只修改超类未设置的属性。
onInitializeAccessibilityNodeInfo()
(API 级别 14)此方法为无障碍服务提供有关视图状态的信息。默认的 View 实现具有一组标准的视图属性,但如果您的自定义视图提供除了简单的 TextViewButton 之外的其他互动控件,则您应替换此方法并将有关视图的其他信息设置到由此方法处理的 AccessibilityNodeInfo 对象中。
onRequestSendAccessibilityEvent()
(API 级别 14)系统会在您的视图的子级生成 AccessibilityEvent 时调用此方法。通过此步骤,父视图可以使用其他信息修改无障碍事件。仅当您的自定义视图具有子视图且父视图可以向无障碍事件提供有助于无障碍服务的上下文信息时,才应实现此方法。

为了针对自定义视图支持这些无障碍方法,您应采用以下方法之一:

  • 如果您的应用以 Android 4.0(API 级别 14)及更高版本为目标平台,请直接在您的自定义视图类中替换并实现上面列出的无障碍方法。
  • 如果您的自定义视图旨在与 Android 1.6(API 级别 4)及以上版本兼容,请在您的项目中添加 Android 支持库修订版 5 或更高版本。然后,在您的自定义视图类中调用 ViewCompat.setAccessibilityDelegate() 方法来实现上述无障碍方法。如需查看此方法的示例,请参阅 <sdk>/extras/android/support/v4/samples/Support4Demos/ 中的 Android 支持库(修订版 5 或更高版本)示例 AccessibilityDelegateSupportActivity

无论是哪种情况,您都应为自定义视图类实现以下无障碍方法:

如需详细了解如何实现这些方法,请参阅填充无障碍事件

发送无障碍事件

根据您的自定义视图的具体情况,可能需要在不同的时间发送 AccessibilityEvent 对象,或者为默认实现未处理的事件发送这些对象。View 类提供以下事件类型的默认实现:

注意:悬停事件与“触摸浏览”功能相关联,后者会将这些事件用作触发器,为界面元素提供声音提示。

一般来说,只要自定义视图的内容发生更改,您就应该发送 AccessibilityEvent。例如,如果您要实现一个自定义滑块条,该滑块条可让用户通过按向左或向右箭头来选择数值,则每当滑块值发生更改时,自定义视图都应发出 TYPE_VIEW_TEXT_CHANGED 类型的事件。以下示例代码演示了如何使用 sendAccessibilityEvent() 方法报告此事件。

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when(keyCode) {
        KeyEvent.KEYCODE_DPAD_LEFT -> {
            currentValue--
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
            true
        }
        ...
    }
}

Java

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

填充无障碍事件

每个 AccessibilityEvent 都有一组描述视图当前状态的必需属性。这些属性包括视图的类名称、内容说明和勾选状态等。AccessibilityEvent 参考文档中介绍了每个事件类型所需的具体属性。View 实现提供了这些属性的默认值。包括类名称和事件时间戳在内的很多值都是自动提供的。如果您要创建自定义视图组件,则必须提供一些与视图内容及特性有关的信息。这些信息可能像按钮标签一样简单,但也可能包含您要添加到事件中的其他状态信息。

使用自定义视图向无障碍服务提供信息的最低要求是实现 dispatchPopulateAccessibilityEvent()。此方法由系统调用以请求 AccessibilityEvent 的信息,并使您的自定义视图与 Android 1.6(API 级别 4)及更高版本上的无障碍服务兼容。以下示例代码演示了此方法的基本实现。

Kotlin

override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean {
    // Call the super implementation to populate its text to the event, which
    // calls onPopulateAccessibilityEvent() on API Level 14 and up.
    return super.dispatchPopulateAccessibilityEvent(event).let { completed ->

        // In case this is running on a API revision earlier that 14, check
        // the text content of the event and add an appropriate text
        // description for this custom view:
        if (text?.isNotEmpty() == true) {
            event.text.add(text)
            true
        } else {
            completed
        }
    }
}

Java

@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    // Call the super implementation to populate its text to the event, which
    // calls onPopulateAccessibilityEvent() on API Level 14 and up.
    boolean completed = super.dispatchPopulateAccessibilityEvent(event);

    // In case this is running on a API revision earlier that 14, check
    // the text content of the event and add an appropriate text
    // description for this custom view:
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        event.getText().add(text);
        return true;
    }
    return completed;
}

对于 Android 4.0(API 级别 14)及更高版本,请使用 onPopulateAccessibilityEvent()onInitializeAccessibilityEvent() 方法填充或修改 AccessibilityEvent 中的信息。onPopulateAccessibilityEvent() 方法专门用于添加或修改事件的文字内容,这些内容会被 TalkBack 等无障碍服务转为声音提示。使用 onInitializeAccessibilityEvent() 方法填充有关事件的其他信息,如视图的选择状态。

此外,还应实现 onInitializeAccessibilityNodeInfo() 方法。无障碍服务使用此方法填充的 AccessibilityNodeInfo 对象调查在接收到该事件后生成无障碍事件的视图层次结构,以获取更详细的上下文信息并向用户提供相应的反馈。

以下示例代码展示了如何使用 ViewCompat.setAccessibilityDelegate() 替换这三种方法。注意,此示例代码要求已将 API 级别 4(修订版 5 或更高版本)的 Android 支持库添加到您的项目中。

Kotlin

ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {

    override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
        super.onPopulateAccessibilityEvent(host, event)
        // We call the super implementation to populate its text for the
        // event. Then we add our text not present in a super class.
        // Very often you only need to add the text for the custom view.
        if (text?.isNotEmpty() == true) {
            event.text.add(text)
        }
    }

    override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {
        super.onInitializeAccessibilityEvent(host, event);
        // We call the super implementation to let super classes
        // set appropriate event properties. Then we add the new property
        // (checked) which is not supported by a super class.
        event.isChecked = isChecked()
    }

    override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
        super.onInitializeAccessibilityNodeInfo(host, info)
        // We call the super implementation to let super classes set
        // appropriate info properties. Then we add our properties
        // (checkable and checked) which are not supported by a super class.
        info.isCheckable = true
        info.isChecked = isChecked()
        // Quite often you only need to add the text for the custom view.
        if (text?.isNotEmpty() == true) {
            info.text = text
        }
    }
})

Java

ViewCompat.setAccessibilityDelegate(new AccessibilityDelegateCompat() {
    @Override
    public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(host, event);
        // We call the super implementation to populate its text for the
        // event. Then we add our text not present in a super class.
        // Very often you only need to add the text for the custom view.
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            event.getText().add(text);
        }
    }
    @Override
    public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(host, event);
        // We call the super implementation to let super classes
        // set appropriate event properties. Then we add the new property
        // (checked) which is not supported by a super class.
        event.setChecked(isChecked());
    }
    @Override
    public void onInitializeAccessibilityNodeInfo(View host,
            AccessibilityNodeInfoCompat info) {
        super.onInitializeAccessibilityNodeInfo(host, info);
        // We call the super implementation to let super classes set
        // appropriate info properties. Then we add our properties
        // (checkable and checked) which are not supported by a super class.
        info.setCheckable(true);
        info.setChecked(isChecked());
        // Quite often you only need to add the text for the custom view.
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            info.setText(text);
        }
    }
}

您可以直接在自定义视图类中实现这些方法。如需查看此方法的另一个示例,请参阅 <sdk>/extras/android/support/v4/samples/Support4Demos/ 中的 Android 支持库(修订版 5 或更高版本)示例 AccessibilityDelegateSupportActivity

提供自定义的无障碍功能上下文

在 Android 4.0(API 级别 14)中,框架经过增强,可允许无障碍服务检查生成无障碍事件的界面组件的容器视图层次结构。经过增强之后,无障碍服务可以提供更丰富的上下文信息以帮助用户。

在某些情况下,无障碍服务无法从视图层次结构中获取足够的信息。例如,日历控件等自定义界面控件是一个具有两个或多个单独可点击区域的控件。在这种情况下,服务无法获取足够的信息,因为可点击的子部分不是视图层次结构的一部分。

图 1. 具有可选日期元素的自定义日历视图。

在图 1 所示的示例中,整个日历以单个视图的形式实现,因此如果您不执行任何其他操作,无障碍服务将无法接收到足够的与视图内容和用户在视图中所做选择有关的信息。例如,如果用户点击包含 17 的日期,则无障碍服务框架只会接收整个日历控件的说明信息。在这种情况下,TalkBack 无障碍服务只会读出“日历”,稍微好点儿的话,或许会读出“四月日历”,而用户可能会好奇究竟选中了哪一天。

为了在此类情况下为无障碍服务提供足够的上下文信息,框架提供了一种方法来指定虚拟的视图层次结构。虚拟视图层次结构可让应用开发者为无障碍服务提供补充性视图层次结构,该视图层次结构更接近屏幕上的实际信息。此方法可使无障碍服务为用户提供更实用的上下文信息。

可能需要虚拟视图层次结构的另一种情况是界面包含一组功能密切相关的控件(视图),其中在一个控件上的操作会影响一个或多个元素的内容,例如具有单独的向上和向下按钮的数字选择器。在这种情况下,无障碍服务无法获取足够的信息,因为针对一个控件的操作会更改另一个控件中的内容,并且这些控件之间的关系对无障碍服务来说可能不明显。如需处理这种情况,请使用包含视图对相关控件进行分组,并从此容器提供一个虚拟视图层次结构,以清楚地表示控件提供的信息和行为。

为了为视图提供一个虚拟视图层次结构,请在自定义视图或视图组中替换 getAccessibilityNodeProvider() 方法并返回 AccessibilityNodeProvider 的实现。 通过将支持库ViewCompat.getAccessibilityNodeProvider() 方法结合使用并提供具有 AccessibilityNodeProviderCompat 的实现,您可以实现与 Android 1.6 及更高版本兼容的虚拟视图层次结构。

为了简化向无障碍服务提供信息和管理无障碍服务焦点的多个方面,请考虑改为实现 ExploreByTouchHelper,它可提供 AccessibilityNodeProviderCompat,并且可以以视图的 AccessibilityDelegateCompat 的形式附加。 有关示例,请查看 ExploreByTouchHelperActivityExploreByTouchHelper 也可供框架 widget(例如 CalendarView)通过其子视图 SimpleMonthView 使用。

处理自定义触摸事件

自定义视图控件可能需要非标准的触摸事件行为,如以下示例所示。

定义基于点击的操作

如果您的微件使用 OnClickListenerOnLongClickListener 接口,则系统会为您处理 ACTION_CLICKACTION_LONG_CLICK 操作。但是,如果您的应用使用依赖于 OnTouchListener 接口的自定义程度更高的微件,您需要为基于点击的无障碍操作定义自定义处理程序。为此,请针对每个操作调用 replaceAccessibilityAction() 方法,如以下代码段所示:

Kotlin

// Assumes that the widget is designed to select text when tapped and select
// all text when long-tapped. In its strings.xml file, this app has set
// "select" to "Select" and "select_all" to "Select all", respectively.
ViewCompat.replaceAccessibilityAction(
            WIDGET,
            ACTION_CLICK,
            context.getString(R.string.select)
) { view, commandArguments ->
    selectText()
}

ViewCompat.replaceAccessibilityAction(
            WIDGET,
            ACTION_LONG_CLICK,
            context.getString(R.string.select_all)
) { view, commandArguments ->
    selectAllText()
}

Java

// Assumes that the widget is designed to select text when tapped and select
// all text when long-tapped. In its strings.xml file, this app has set
// "select" to "Select" and "select_all" to "Select all", respectively.
ViewCompat.replaceAccessibilityAction(WIDGET, ACTION_CLICK,
        context.getString(R.string.select),
        (view, commandArguments) -> {
            selectText();
        });

ViewCompat.replaceAccessibilityAction(WIDGET, ACTION_LONG_CLICK,
        context.getString(R.string.select_all),
        (view, commandArguments) -> {
            selectAllText();
        });

创建自定义点击事件

自定义控件可能会使用 onTouchEvent(MotionEvent) 监听器方法来检测 ACTION_DOWNACTION_UP 事件并触发特殊点击事件。为了保持与无障碍服务兼容,处理此自定义点击事件的代码必须执行以下操作:

  1. 为经过解释的点击操作生成相应的 AccessibilityEvent
  2. 启用无障碍服务,为无法使用触摸屏的用户执行自定义点击操作。

为了高效满足这些要求,您的代码应替换 performClick() 方法,为此,必须调用此方法的超类实现,然后执行点击事件所需的任何操作。当检测到自定义点击操作时,该代码应调用您的 performClick() 方法。以下代码示例演示了此模式。

Kotlin

class CustomTouchView(context: Context) : View(context) {

    var downTouch = false

    override fun onTouchEvent(event: MotionEvent): Boolean {
        super.onTouchEvent(event)

        // Listening for the down and up touch events
        return when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                downTouch = true
                true
            }

            MotionEvent.ACTION_UP -> if (downTouch) {
                downTouch = false
                performClick() // Call this method to handle the response, and
                // thereby enable accessibility services to
                // perform this action for a user who cannot
                // click the touchscreen.
                true
            } else {
                false
            }

            else -> false  // Return false for other touch events
        }
    }

    override fun performClick(): Boolean {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any
        super.performClick()

        // Handle the action for the custom click here

        return true
    }
}

Java

class CustomTouchView extends View {

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

    boolean downTouch = false;

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

        // Listening for the down and up touch events
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (downTouch) {
                    downTouch = false;
                    performClick(); // Call this method to handle the response, and
                                    // thereby enable accessibility services to
                                    // perform this action for a user who cannot
                                    // click the touchscreen.
                    return true;
                }
        }
        return false; // Return false for other touch events
    }

    @Override
    public boolean performClick() {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any
        super.performClick();

        // Handle the action for the custom click here

        return true;
    }
}

上述模式使用 performClick() 方法生成无障碍事件并为无障碍服务提供入口点以使其代表用户执行自定义点击事件,以此来确保此自定义点击事件与无障碍服务兼容。

注意:如果您的自定义视图具有不同的可点击区域(如自定义日历视图),您必须通过替换自定义视图中的 getAccessibilityNodeProvider() 来实现虚拟视图层次结构,这样才能与无障碍服务兼容。