Принципы улучшения специальных возможностей приложения (Views)

Основные понятия и реализация Jetpack Compose

Чтобы помочь пользователям с особыми потребностями, фреймворк Android позволяет создавать специальные возможности, которые могут показывать контент из приложений и управлять ими от имени пользователей.

В Android есть несколько системных сервисов специальных возможностей, в том числе:

  • TalkBack помогает людям с нарушениями зрения. Она озвучивает контент синтезированным голосом и выполняет действия в приложении в ответ на жесты пользователя.
  • Switch Access помогает людям с двигательными нарушениями. Он выделяет интерактивные элементы и выполняет действия в ответ на нажатие кнопки пользователем. Она позволяет управлять устройством с помощью всего одной или двух кнопок.

Чтобы пользователи с особыми потребностями могли успешно работать с вашим приложением, оно должно соответствовать рекомендациям, описанным на этой странице и в статье Как сделать приложение более доступным.

Элементы Label

Важно, чтобы у каждого интерактивного элемента интерфейса в приложении был ярлык с описанием его назначения. Программы чтения с экрана, такие как 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. Подробная информация приведена в разделе ниже.

Пары элементов, в которых один описывает другой.

Обычно у элемента EditText есть соответствующий объект View, который описывает, что пользователи должны ввести в элемент EditText. Для этого нужно задать атрибут android:labelFor объекта View.

Пример разметки таких пар элементов приведен в следующем фрагменте кода:

<!-- 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) и более поздних версиях можно добавлять специальные названия для панелей экрана. С точки зрения специальных возможностей панель – это визуально различимая часть окна, например содержимое фрагмента. Чтобы специальные возможности могли распознавать поведение панели, похожее на поведение окна, дайте панелям приложения описательные названия. Это позволяет сервисам специальных возможностей предоставлять пользователям более подробную информацию об изменениях в панели.

Чтобы указать название панели, используйте атрибут 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 и Switch 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. Поэтому вы можете сосредоточиться на расширении количества возможных состояний с двух до трех.

Как определить специальные события

Расширяя системный виджет, вы, скорее всего, меняете способ взаимодействия пользователей с ним. Важно определить эти изменения взаимодействия, чтобы специальные возможности могли обновлять виджет вашего приложения так, как если бы пользователь взаимодействовал с виджетом напрямую.

Общее правило заключается в том, что для каждого переопределяемого обратного вызова на основе представления необходимо также переопределить соответствующее действие специальных возможностей, переопределив 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;
    }
}

Дополнительные ресурсы

Чтобы узнать больше о том, как сделать приложение более доступным, ознакомьтесь со следующими дополнительными ресурсами:

Практические работы

Записи в блоге