让自定义视图更易于访问

如果您的应用需要自定义视图组件,您必须使该视图更易于访问。以下步骤可以提高自定义视图的无障碍功能,具体说明如下:

  • 处理定向控制器点击。
  • 实现无障碍 API 方法。
  • 发送特定于您的自定义视图的 AccessibilityEvent 对象。
  • 为您的视图填充 AccessibilityEventAccessibilityNodeInfo

处理定向控制器点击

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

您的自定义控件必须将 KEYCODE_ENTER 事件与 KEYCODE_DPAD_CENTER 相同对待。这使得用户更容易使用全键盘进行交互。

实现无障碍 API 方法

无障碍事件是关于用户与应用可视化界面组件交互的消息。这些消息由无障碍服务处理,这些服务使用事件中的信息来生成补充反馈和提示。无障碍方法是 ViewView.AccessibilityDelegate 类的一部分。这些方法如下:

dispatchPopulateAccessibilityEvent()
当您的自定义视图生成无障碍事件时,系统会调用此方法。此方法的默认实现会为此视图调用 onPopulateAccessibilityEvent(),然后为此视图的每个子视图调用 dispatchPopulateAccessibilityEvent() 方法。
onInitializeAccessibilityEvent()
系统调用此方法以获取除文本内容之外的视图状态的其他信息。如果您的自定义视图提供了超出简单 TextViewButton 的交互式控制,请重写此方法并使用此方法设置有关视图的其他信息(例如密码字段类型、复选框类型,或提供用户交互或反馈的状态到事件中)。如果您重写此方法,请调用其超级实现,并且只修改超类未设置的属性。
onInitializeAccessibilityNodeInfo()
此方法向无障碍服务提供有关视图状态的信息。View 的默认实现具有一组标准的视图属性,但如果您的自定义视图提供了超出简单 TextViewButton 的交互式控制,请重写此方法并将有关视图的其他信息设置到此方法处理的 AccessibilityNodeInfo 对象中。
onPopulateAccessibilityEvent()
此方法为您的视图设置 AccessibilityEvent 的语音文本提示。如果视图是生成无障碍事件的视图的子视图,也会调用此方法。
onRequestSendAccessibilityEvent()
当您的视图的子视图生成 AccessibilityEvent 时,系统会调用此方法。此步骤允许父视图用附加信息修正无障碍事件。仅当您的自定义视图可以有子视图且父视图可以向无障碍事件提供对无障碍服务有用的上下文信息时,才实现此方法。
sendAccessibilityEvent()
当用户对视图执行操作时,系统会调用此方法。该事件根据用户操作类型进行分类,例如 TYPE_VIEW_CLICKED。通常,每当自定义视图的内容发生变化时,您都必须发送一个 AccessibilityEvent
sendAccessibilityEventUnchecked()
此方法用于当调用代码需要直接控制设备上无障碍功能是否启用(AccessibilityManager.isEnabled())的检查时。如果您实现此方法,则无论系统设置如何,都应像无障碍功能已启用一样执行调用。通常,您无需为自定义视图实现此方法。

为了支持无障碍功能,请直接在您的自定义视图类中重写并实现上述无障碍方法。

至少,为您的自定义视图类实现以下无障碍方法:

  • dispatchPopulateAccessibilityEvent()
  • onInitializeAccessibilityEvent()
  • onInitializeAccessibilityNodeInfo()
  • onPopulateAccessibilityEvent()

有关实现这些方法的更多信息,请参阅关于填充无障碍事件的部分。

发送无障碍事件

根据您的自定义视图的具体情况,它可能需要在不同时间或针对默认实现未处理的事件发送 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 实现为这些必需属性提供默认值。其中许多值,包括类名和事件时间戳,都是自动提供的。如果您正在创建自定义视图组件,则必须提供有关视图内容和特征的信息。这些信息可以像按钮标签一样简单,也可以包含您想要添加到事件中的其他状态信息。

使用 onPopulateAccessibilityEvent()onInitializeAccessibilityEvent() 方法来填充或修改 AccessibilityEvent 中的信息。专门使用 onPopulateAccessibilityEvent() 方法来添加或修改事件的文本内容,这些内容将由 TalkBack 等无障碍服务转换为语音提示。使用 onInitializeAccessibilityEvent() 方法来填充有关事件的其他信息,例如视图的选中状态。

此外,实现 onInitializeAccessibilityNodeInfo() 方法。无障碍服务使用此方法填充的 AccessibilityNodeInfo 对象来调查接收到无障碍事件后生成该事件的视图层次结构,并向用户提供适当的反馈。

以下代码示例展示了如何在视图中重写这三个方法:

Kotlin

override fun onPopulateAccessibilityEvent(event: AccessibilityEvent?) {
    super.onPopulateAccessibilityEvent(event)
    // Call the super implementation to populate its text for the
    // event. Then, add text not present in a super class.
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        event?.text?.add(text)
    }
}

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
    super.onInitializeAccessibilityEvent(event)
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event?.isChecked = isChecked()
}

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
    super.onInitializeAccessibilityNodeInfo(info)
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info?.isCheckable = true
    info?.isChecked = isChecked()
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        info?.text = text
    }
}

Java

@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    super.onPopulateAccessibilityEvent(event);
    // Call the super implementation to populate its text for the
    // event. Then, add the text not present in a super class.
    // You typically 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(AccessibilityEvent event) {
    super.onInitializeAccessibilityEvent(event);
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event.setChecked(isChecked());
}

@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    super.onInitializeAccessibilityNodeInfo(info);
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info.setCheckable(true);
    info.setChecked(isChecked());
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        info.setText(text);
    }
}

您可以在自定义视图类中直接实现这些方法。

提供自定义无障碍上下文

无障碍服务可以检查生成无障碍事件的用户界面组件所包含的视图层次结构。这使得无障碍服务可以提供更丰富的上下文信息以帮助用户。

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

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

在图 1 的示例中,整个日历都作为单个视图实现,因此除非开发者提供额外信息,否则无障碍服务无法获取有关视图内容和用户在视图中选择的足够信息。例如,如果用户点击标记为17的日期,无障碍框架只会收到整个日历控件的描述信息。在这种情况下,TalkBack 无障碍服务会宣布“日历”或“四月日历”,用户不知道选择了哪一天。

为了在这种情况下为无障碍服务提供足够的上下文信息,框架提供了一种指定虚拟视图层次结构的方法。虚拟视图层次结构是一种应用开发者可以向无障碍服务提供补充视图层次结构的方法,该层次结构更精确地匹配屏幕上的信息。这种方法允许无障碍服务向用户提供更有用的上下文信息。

需要虚拟视图层次结构的另一种情况是包含一组功能紧密相关的 View 控件的用户界面,其中对一个控件的操作会影响一个或多个元素的内容(例如带独立上下按钮的数字选择器)。在这种情况下,无障碍服务无法获得足够的信息,因为对一个控件的操作会更改另一个控件中的内容,并且这些控件之间的关系可能对服务不明显。

为了处理这种情况,将相关控件与包含视图进行分组,并从该容器提供虚拟视图层次结构,以清晰地表示控件提供的信息和行为。

要为视图提供虚拟视图层次结构,请在自定义视图或视图组中重写 getAccessibilityNodeProvider() 方法并返回 AccessibilityNodeProvider 的实现。您可以通过使用支持库和 ViewCompat.getAccessibilityNodeProvider() 方法并提供 AccessibilityNodeProviderCompat 的实现来创建虚拟视图层次结构。

为了简化向无障碍服务提供信息和管理无障碍焦点的任务,您可以转而实现 ExploreByTouchHelper。它提供了 AccessibilityNodeProviderCompat,并且可以通过调用 setAccessibilityDelegate 将其作为视图的 AccessibilityDelegateCompat 附加。有关示例,请参阅 ExploreByTouchHelperActivityExploreByTouchHelper 也被框架微件(例如 CalendarView)通过其子视图 SimpleMonthView 使用。

处理自定义触摸事件

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

定义基于点击的操作

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

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Assumes that the widget is designed to select text when tapped, and selects
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_CLICK,
        getString(R.string.select)
    ) { view, commandArguments ->
        selectText()
    }

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

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Assumes that the widget is designed to select text when tapped, and select
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_CLICK,
            getString(R.string.select),
            (view, commandArguments) -> selectText());

    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_LONG_CLICK,
            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
                // enable accessibility services to
                // perform this action for a user who can't
                // tap 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
                                    // enable accessibility services to
                                    // perform this action for a user who can't
                                    // tap 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() 方法生成无障碍事件并为无障碍服务提供一个入口点,使其能够代表执行自定义点击事件的用户采取行动,从而有助于确保自定义点击事件与无障碍服务兼容。