Принципы повышения доступности приложений (Views)

Концепции и реализация Jetpack Compose

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

Android предоставляет ряд системных служб специальных возможностей, в том числе следующие:

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

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

Элементы меток

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

В этой ситуации атрибут android:labelFor объекта View должен быть установлен на идентификатор элемента 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 — чтобы каждый дочерний элемент был однозначно идентифицирован.

Для этого задайте описание содержимого в рамках реализации вашего адаптера, как показано в следующем фрагменте кода:

Котлин

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

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

Например, если ваше приложение позволяет пользователям проводить пальцем по элементу, вы также можете предоставить доступ к этой функциональности через пользовательское действие специальных возможностей, следующим образом:

Котлин

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

Котлин

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;
    }
}

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

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

Кодлабс

Сообщения в блоге