改进应用无障碍功能要遵循的原则

为了帮助用户满足无障碍功能需求,您可以使用 Android 框架创建无障碍服务,这样既可将应用中的内容呈现给用户,又可代表用户操作应用。

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

  • TalkBack:帮助弱视或失明用户。它通过合成语音读出内容,并在应用中执行操作以响应用户手势。
  • 开关控制:帮助有运动障碍的人。它会突出显示互动元素并执行操作来响应用户按下按钮的操作。用户只需使用一个或两个按钮就能控制设备。

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

以下各部分中介绍的每项最佳实践都可以进一步改进应用的无障碍功能:

标签元素
用户必须能够理解应用中每个有意义的互动式界面元素的内容和用途。
添加无障碍操作
通过添加无障碍操作,您可以让无障碍服务的用户在您的应用中完成关键用户流。
扩展系统 widget
基于框架包含的视图元素构建,而不是创建自己的自定义视图。框架的视图和微件类已经提供了应用所需的大多数无障碍功能。
使用除颜色之外的提示
用户必须能够清楚地区分界面中的各类元素。为此,除了颜色之外,还应使用图案和位置表示这些差异。
让媒体内容使用起来更没有障碍
为应用的视频或音频内容添加说明,这样使用这些内容的用户就不需要完全依赖视觉或听觉提示。

标签元素

请务必针对应用中的每个交互式界面元素为用户提供实用的描述性标签。每个标签都必须解释特定元素的含义和用途。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" ... />

集合中的元素

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

需要特别指出的是,在重复使用的布局内的元素(如 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、Voice Access 或开关控制用户可能需要通过其他方式来完成应用内的特定用户流程。对于与手势相关的操作(例如拖放或滑动),您的应用能够以无障碍服务用户可访问的方式公开这些操作。

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

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

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 action’s 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 对象的行为几乎相同。因此,您的实现可以专注于将可能的状态数量从两种增加到三种。

定义自定义事件

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

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

此原则如何适用于 TriSwitch 对象

与普通的 Switch 对象不同,点按 TriSwitch 对象会循环切换三种可能的状态。因此,需要更新相应的 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;
    }
}

使用除颜色之外的提示

为了帮助有色觉缺陷的用户,请使用除颜色之外的提示区分应用屏幕中的界面元素。这些技术可能包括使用不同的形状或大小、提供文本或视觉图案,或者添加音频或基于触摸(触感反馈)的反馈来标记元素的差异。

图 1 显示了一个 Activity 的两个版本。一个版本仅使用颜色区分工作流程中两种可能的操作。另一个版本采用了最佳做法:除了颜色之外,还使用了形状和文字来突出两个选项之间的差异:

图 1. 仅使用颜色创建界面元素(左侧)和使用颜色、形状和文本(右侧)的示例。

让媒体内容使用起来更没有障碍

如果您开发的应用包含视频剪辑或音频录音等媒体内容,应设法为具有不同类型的无障碍功能需求的用户提供支持,让他们能够理解此类内容。特别是,我们建议您做到以下几点:

  • 添加可让用户暂停或停止播放媒体、调节音量以及切换字幕的控件。
  • 如果视频提供的信息对于完成工作流程至关重要,请以其他格式提供相同的内容,例如转写内容。

其他资源

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

Codelab

博文