使自定义视图更易访问

如果您的应用程序需要 自定义视图组件,您必须使该视图更易访问。以下步骤可以改善自定义视图的无障碍性,如本页所述

  • 处理方向控制器点击。
  • 实现无障碍 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() 方法来生成辅助功能事件,并为辅助功能服务提供一个入口点,以便代表执行自定义点击事件的用户采取行动,前面的模式有助于确保自定义点击事件与辅助功能服务兼容。