Концепции и реализация 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 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 :
Котлин
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; } }
Дополнительные ресурсы
Чтобы узнать больше о том, как сделать ваше приложение более доступным, ознакомьтесь со следующими дополнительными ресурсами: