让自定义视图更易于使用(视图)

概念和 Jetpack Compose 实现

如果您的应用需要自定义视图组件,就必须让视图使用起来更没有障碍。如本页所述,您可以按照以下步骤来改进自定义视图的无障碍功能:

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

处理方向控制器点击操作

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

您的自定义控件必须将 KEYCODE_ENTER 事件视为与 KEYCODE_DPAD_CENTER 相同。这样可以让用户更轻松地使用全键盘进行互动。

实现无障碍功能 API 方法

无障碍事件是有关用户与应用中的可视化界面组件互动的消息。这些消息由无障碍服务处理,后者使用这些事件中的信息生成补充反馈和提示。无障碍功能方法是 View 和 View.AccessibilityDelegate 类的一部分。具体方法如下所示:

dispatchPopulateAccessibilityEvent()
系统会在自定义视图生成无障碍事件时调用此方法。该方法的默认实现会为此视图调用 onPopulateAccessibilityEvent(),然后为此视图的每个子级调用 dispatchPopulateAccessibilityEvent() 方法。
onInitializeAccessibilityEvent()
除了文字内容之外,系统还会调用此方法来获取有关视图状态的其他信息。如果自定义视图提供除了简单的 TextView 或 Button 之外的其他互动控制,请替换此方法并将有关视图的其他信息(如密码字段类型、复选框类型或者提供用户互动或反馈的状态)设置到使用此方法的事件中。如果您替换此方法,则需调用其超类实现,然后只修改超类未设置的属性。
onInitializeAccessibilityNodeInfo()
此方法可为无障碍服务提供有关视图状态的信息。默认的 View 实现具有一组标准的视图属性,但如果自定义视图提供除了简单的 TextView 或 Button 之外的其他互动控制,则需替换此方法并将有关视图的其他信息设置到由此方法处理的 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 的形式附加。有关示例,请查看 ExploreByTouchHelperActivity。ExploreByTouchHelper 也可供框架 widget(例如 CalendarView)通过其子视图 SimpleMonthView 使用。

处理自定义触摸事件

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

定义基于点击的操作

如果您的 widget 使用 OnClickListener 或 OnLongClickListener 接口,则系统会为您处理 ACTION_CLICK 和 ACTION_LONG_CLICK 操作。如果您的应用使用依赖于 OnTouchListener 接口的自定义程度更高的 widget,您需要为基于点击的无障碍操作定义自定义处理程序。为此,请针对每个操作调用 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_DOWN 和 ACTION_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() 方法生成无障碍事件并为无障碍服务提供入口点以使其代表用户执行自定义点击事件,有助于确保该自定义点击事件与无障碍服务兼容。