改进应用无障碍功能时需遵循的原则(视图)

概念和 Jetpack Compose 实现

为了帮助有无障碍功能需求的用户,Android 框架允许您创建无障碍服务,以便向用户呈现应用中的内容,并代表用户操作应用。

Android 提供了一些系统无障碍服务,其中包括:

为了帮助有无障碍功能需求的用户顺利使用您的应用,您的 应用必须遵循本页中介绍的最佳实践,这些实践基于 让应用使用起来更没有障碍中所述的准则。

标签元素

请务必针对应用中的每个互动界面元素,为用户提供实用且描述性的标签。每个标签都必须说明特定元素的含义和用途。TalkBack 等屏幕阅读器可以向用户读出这些标签。

在大多数情况下,您可以在包含界面元素的布局资源文件中指定该元素的说明。通常,您可以使用 contentDescription属性添加标签,如让应用 使用起来更没有障碍指南中所述。以下各部分介绍了其他几种标签添加技术。

可修改的元素

为可修改的元素(如 EditText对象)添加标签时,除了让相应的示例文字可由屏幕阅读器读出之外,最好还在元素本身中显示可作为有效输入示例的文字。在这些情况下,您可以使用 android:hint 属性,如以下代码段所示:

<!-- The hint text for en-US locale would be
     "Apartment, suite, or building". -->
<EditText
   android:id="@+id/addressLine2"
   android:hint="@string/aptSuiteBuilding" ... />

在这种情况下,View 对象的 android:labelFor 属性 必须设置为 EditText 元素的 ID。如需了解详情,请参阅下一部分 。

相互描述的元素对

EditText 元素通常具有相应的 View 对象,用于说明用户必须 在 EditText 元素中输入的内容。您可以通过设置 View 对象的 android:labelFor 属性来指明这种关系。

下面的代码段显示了为此类元素对添加标签的示例:

<!-- Label text for en-US locale would be "Username:" -->
<TextView
   android:id="@+id/usernameLabel" ...
   android:text="@string/username"
   android:labelFor="@+id/usernameEntry" />

<EditText
   android:id="@+id/usernameEntry" ... />

<!-- Label text for en-US locale would be "Password:" -->
<TextView
   android:id="@+id/passwordLabel" ...
   android:text="@string/password
   android:labelFor="@+id/passwordEntry" />

<EditText
   android:id="@+id/passwordEntry"
   android:inputType="textPassword" ... />

集合中的元素

为集合的元素添加标签时,每个标签都必须是唯一的。 这样,系统的无障碍服务在读出标签时就可以特指屏幕上的 1 个元素。这种对应关系可让用户知道他们何时在界面中循环浏览,或何时将焦点移至他们已发现的元素。

需要特别注意的是,您应在 重用布局内的元素(如 RecyclerView 对象)中添加更多文字或上下文信息,以便能够唯一标识每个子元素。

为此,请在适配器实现中设置内容说明,如以下代码段所示:

Kotlin

data class MovieRating(val title: String, val starRating: Integer)

class MyMovieRatingsAdapter(private val myData: Array<MovieRating>):
        RecyclerView.Adapter<MyMovieRatingsAdapter.MyRatingViewHolder>() {

    class MyRatingViewHolder(val ratingView: ImageView) :
            RecyclerView.ViewHolder(ratingView)

    override fun onBindViewHolder(holder: MyRatingViewHolder, position: Int) {
        val ratingData = myData[position]
        holder.ratingView.contentDescription = "Movie ${position}: " +
                "${ratingData.title}, ${ratingData.starRating} stars"
    }
}

Java

public class MovieRating {
    private String title;
    private int starRating;
    // ...
    public String getTitle() { return title; }
    public int getStarRating() { return starRating; }
}

public class MyMovieRatingsAdapter
        extends RecyclerView.Adapter<MyAdapter.MyRatingViewHolder> {
    private MovieRating[] myData;


    public static class MyRatingViewHolder extends RecyclerView.ViewHolder {
        public ImageView ratingView;
        public MyRatingViewHolder(ImageView iv) {
            super(iv);
            ratingView = iv;
        }
    }

    @Override
    public void onBindViewHolder(MyRatingViewHolder holder, int position) {
        MovieRating ratingData = myData[position];
        holder.ratingView.setContentDescription("Movie " + position + ": " +
                ratingData.getTitle() + ", " + ratingData.getStarRating() +
                " stars")
    }
}

相关内容组

如果应用显示的多个界面元素构成一个自然组(如歌曲的详细信息或消息的属性),应将这些元素整理到一个容器中,该容器通常是 ViewGroup 的子类。将容器 对象 android:screenReaderFocusable 属性设为 true,并将每个内部对象的 android:focusable 属性设为 false。这样,无障碍服务就可以在单次语音中逐个读出内部元素的内容说明。这样整合相关元素有助于使用辅助技术的用户更高效地发现屏幕上的信息。

以下代码段包含彼此相关的内容片段,因此容器元素(即 ConstraintLayout 的实例)的 android:screenReaderFocusable 属性设为 true,每个内部 TextView 元素的 android:focusable 属性设为 false

<!-- In response to a single user interaction, accessibility services announce
     both the title and the artist of the song. -->
<ConstraintLayout
    android:id="@+id/song_data_container" ...
    android:screenReaderFocusable="true">

    <TextView
        android:id="@+id/song_title" ...
        android:focusable="false"
        android:text="@string/my_song_title" />
    <TextView
        android:id="@+id/song_artist"
        android:focusable="false"
        android:text="@string/my_songwriter" />
</ConstraintLayout>

由于无障碍服务在单次语音中读出内部元素的说明,因此务必确保每条说明都简明扼要地传达出元素的含义。

注意 :一般来说,您应避免通过汇总子项的文字来为组创建内容说明。这样做会使组的说明变得脆弱,并且当子项的文字发生变化时,组的说明可能不再与可见文字匹配。

在列表或网格上下文中,屏幕阅读器可能会合并列表或网格元素的子文本节点中的文字。最好避免修改此通知。

嵌套组

如果应用的界面显示多维信息(如节日活动的每日列表),应对内部组容器使用 android:screenReaderFocusable 属性。这种标签添加方案可以很好地平衡发现屏幕内容所需的语音条数与每条语音的长度。

以下代码段展示了一种为较大的组内的组添加标签的方法:

<!-- In response to a single user interaction, accessibility services
     announce the events for a single stage only. -->
<ConstraintLayout
    android:id="@+id/festival_event_table" ... >
    <ConstraintLayout
        android:id="@+id/stage_a_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage A. -->

    </ConstraintLayout>
    <ConstraintLayout
        android:id="@+id/stage_b_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage B. -->

    </ConstraintLayout>
</ConstraintLayout>

文字中的标题

某些应用使用标题总结屏幕上显示的多组文字。如果特定的 View 元素表示一个标题,您可以通过将该元素的 android:accessibilityHeading 属性设为 true,表明它的无障碍服务用途。

无障碍服务的用户可以选择浏览标题,而不是浏览段落或字词。这种灵活性可改善文字浏览体验。

无障碍窗格标题

在 Android 9(API 级别 28)及更高版本中,您可以为屏幕的窗格提供使用起来没有障碍的标题。出于无障碍目的,窗格是窗口中能够从视觉上加以区分的部分,如 Fragment 的内容。为了让无障碍服务能够理解与窗口行为类似的窗格行为,请为应用的窗格指定描述性标题。这样一来,当窗格的外观或内容发生变化时,无障碍服务就可以为用户提供更精细的信息。

如需指定窗格的标题,请使用 android:accessibilityPaneTitle 属性,如以下代码段所示:

<!-- Accessibility services receive announcements about content changes
     that are scoped to either the "shopping cart view" section (top) or
     "browse items" section (bottom) -->
<MyShoppingCartView
     android:id="@+id/shoppingCartContainer"
     android:accessibilityPaneTitle="@string/shoppingCart" ... />

<MyShoppingBrowseView
     android:id="@+id/browseItemsContainer"
     android:accessibilityPaneTitle="@string/browseProducts" ... />

装饰性元素

如果界面中某个元素的存在只是为了让内容看起来间距合理或布局美观 ,请将其 android:importantForAccessibility 属性设为 "no"

添加了 无障碍操作

请务必允许无障碍服务的用户轻松执行应用中的所有用户体验流程。例如,如果用户可以对列表中的项目执行滑动操作,则该操作也应能够通过无障碍服务来实现,以便用户能通过另一种方式完成相同的用户体验流程。

让所有操作都可访问

TalkBack、语音操控、或开关控制的用户可能需要其他方式来完成应用中的某些用户体验流程。对于与拖放或滑动等手势相关联的操作,您的应用可以以无障碍服务用户可以访问的方式公开这些操作。

借助 无障碍操作, 应用可以为用户提供完成操作的其他方式。

例如,如果您的应用允许用户对某个项目执行滑动操作,您还可以通过自定义无障碍操作公开该功能,如下所示:

Kotlin

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive)
) { _, _ ->
    // Same method executed when swiping on itemView
    archiveItem()
    true
}

Java

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive),
    (view, arguments) -> {
        // Same method executed when swiping on itemView
        archiveItem();
        return true;
    }
);

With the custom accessibility action implemented, users can access the action through the actions menu.

Make available actions understandable

When a view supports actions such as touch & hold, an accessibility service such as TalkBack announces it as "Double tap and hold to long press."

This generic announcement doesn't give the user any context about what a touch & hold action does.

To make this announcement more descriptive, you can replace the accessibility actions announcement like so:

Kotlin

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
)

Java

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
);

This results in TalkBack announcing "Double tap and hold to favorite," helping users understand the purpose of the action.

Extend system widgets

Note: When you design your app's UI, use or extend system-provided widgets that are as far down Android's class hierarchy as possible. System-provided widgets that are far down the hierarchy already have most of the accessibility capabilities your app needs. It's easier to extend these system-provided widgets than to create your own from the more generic View, ViewCompat, Canvas, and CanvasCompat classes.

If you must extend View or Canvas directly, which might be necessary for a highly customized experience or a game level, see Make custom views more accessible.

This section uses the example of implementing a special type of Switch called TriSwitch while following best practices around extending system widgets. A TriSwitch object works similarly to a Switch object, except that each instance of TriSwitch allows the user to toggle among three possible states.

Extend from far down the class hierarchy

The Switch object inherits from several framework UI classes in its hierarchy:

View
 TextView
   Button
     CompoundButton
       Switch

新的 TriSwitch 类最好直接从 Switch 类扩展。这样,Android 无障碍功能 框架 就可以提供大多数无障碍功能,这是 TriSwitch 类 所需的:

  • 无障碍操作 :告知系统无障碍服务如何模拟在 TriSwitch 对象上执行的各种可能的用户输入。(继承自 View。)
  • 无障碍事件 :告知无障碍服务当屏幕刷新或更新时,TriSwitch 对象的外观会发生的各种可能的变化方式。(继承自 View。)
  • 特征 :有关每个 TriSwitch 对象的详细信息,例如其显示的任何文字的内容。(继承自 TextView。)
  • 状态信息TriSwitch 对象的当前状态的说明,如“选中”或“未选中”。 (继承自 CompoundButton。)
  • 状态的文字说明 :文字类说明,解释了各种状态的含义。(继承自 Switch。)

继承自 Switch 及其父类的这种行为几乎就是 TriSwitch 对象的行为。因此,您的实现可以专注于将可能的状态数从两个扩展到三个。

定义自定义事件

当您扩展某个系统微件时,可能会改变用户与该微件互动方式的某一方面。请务必定义这些互动变化,以便无障碍服务可以更新应用的微件,就像用户直接与微件互动一样。

一般准则是,对于您替换的每个基于视图的回调, 您还需要通过替换 ViewCompat.replaceAccessibilityAction()重新定义相应的无障碍操作。 在应用的测试中,您可以通过 调用 ViewCompat.performAccessibilityAction()来验证这些重新定义的操作的行为。

此原则如何对 TriSwitch 对象起作用

与普通的 Switch 对象不同,点按 TriSwitch 对象可以循环切换 3 种可能的状态。因此,需要更新相应的 ACTION_CLICK 无障碍操作:

Kotlin

class TriSwitch(context: Context) : Switch(context) {
    // 0, 1, or 2
    var currentState: Int = 0
        private set

    init {
        updateAccessibilityActions()
    }

    private fun updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label) {
            view, args -> moveToNextState()
        })
    }

    private fun moveToNextState() {
        currentState = (currentState + 1) % 3
    }
}

Java

public class TriSwitch extends Switch {
    // 0, 1, or 2
    private int currentState;

    public int getCurrentState() {
        return currentState;
    }

    public TriSwitch() {
        updateAccessibilityActions();
    }

    private void updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label, (view, args) -> moveToNextState());
    }

    private void moveToNextState() {
        currentState = (currentState + 1) % 3;
    }
}

其他资源

如需详细了解如何让您的应用使用起来更没有障碍,请参阅下面列出的其他资源:

Codelab

博文