Введение

«Что такое contentDescription и зачем он нужен?» — вопрос, который открывает тему доступности почти на каждом собеседовании. И тут же выясняется, что большинство кандидатов с опытом 1–2 года ставили этот атрибут «чтобы линт не ругался», не понимая, что за ним стоит целая система.

Доступность (accessibility) в Android — это механизм, который позволяет людям с особенностями зрения, слуха или моторики пользоваться приложением: через экранную озвучку, управление голосом, переключатели. По данным Всемирного банка, ту или иную форму инвалидности имеет около 15% населения мира — это не «три пользователя с Pixel», а огромная аудитория, которая просто уходит из неудобного приложения.

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

Что такое доступность и как она работает

В основе всего лежит понятие accessibility-сервиса — системного компонента, который «читает» интерфейс приложения и помогает пользователю с ним взаимодействовать. Приложение не знает, какой сервис подключён, — оно просто описывает свой UI, а сервис решает, как его представить.

Главные системные сервисы Android, которые стоит знать по именам:

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

Главная идея для собеседования

Android-фреймворк позволяет создавать accessibility-сервисы, которые представляют контент приложения пользователю и управляют приложением от его имени. Задача разработчика — сделать так, чтобы интерфейс был «читаемым» для этих сервисов: у каждого значимого элемента есть описание, роль и состояние.

Из этой модели следует практический вывод: доступность — это не отдельная страница настроек, а качество описания UI. Экран читается не «пикселями», а семантикой: что это за элемент (кнопка, переключатель, заголовок), что он делает, в каком он состоянии. Чем лучше вы описали эти свойства, тем лучше сервис справляется.

Описания элементов: contentDescription и семантика

Когда TalkBack наводит фокус на элемент, он озвучивает его описание. Для текстовых элементов всё просто: система читает сам текст. Поэтому для TextView или Text в Compose отдельное описание не нужно — TalkBack и так озвучит содержимое.

А вот всё, что не является текстом, требует описания вручную. В Views за это отвечает атрибут android:contentDescription, в Compose — параметр contentDescription у Icon, Image и им подобных.

xml
activity_main.xml
<ImageButton
    android:id="@+id/btn_share"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:contentDescription="@string/action_share"
    android:src="@drawable/ic_share" />
kotlin
ShareButton.kt
@Composable
fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

Здесь есть несколько правил, по которым видно «понимающего» кандидата:

  • Описание передаёт назначение, а не внешний вид. Не «синяя иконка стрелки», а «Поделиться». TalkBack и так сообщит тип элемента через роль, поэтому «Кнопка поделиться» — избыточно: достаточно «Поделиться».
  • Каждое описание уникально. Если в списке у всех элементов одинаковое описание, пользователь не поймёт, где он находится. Для элементов в RecyclerView/LazyColumn описание задаётся в адаптере и учитывает данные позиции.
  • Декоративные элементы помечаются как невидимые. Картинка, которая нужна только для красоты, не должна озвучиваться: в Views — android:importantForAccessibility="no", в Compose — contentDescription = null.
kotlin
BannerImage.kt
Image(
    painter = painterResource(R.drawable.banner),
    contentDescription = null // чисто декоративный элемент
)

«Я всем Image поставил contentDescription с текстом вроде „иконка стрелки“». Это шум: TalkBack будет озвучивать мусор вместо смысла. Либо осмысленное описание действия, либо null/importantForAccessibility="no" для декора. Третьего не дано.

Отдельно про пары «поле + подпись»: если подпись к EditText лежит отдельным TextView, свяжите их атрибутом android:labelFor — тогда TalkBack озвучит подпись при фокусе на поле, даже если визуально это два разных элемента.

Группы, заголовки и порядок фокуса

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

Классический пример: карточка песни с названием и исполнителем. Озвучивать их по отдельности неудобно — пользователь хочет услышать «Название, Исполнитель» одним сообщением. В Views для этого собирают элементы в группу: контейнеру — android:screenReaderFocusable="true", внутренним элементам — android:focusable="false".

xml
item_song.xml
<LinearLayout
    android:id="@+id/song_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:screenReaderFocusable="true">

    <TextView
        android:id="@+id/song_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false" />

    <TextView
        android:id="@+id/song_artist"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false" />
</LinearLayout>

В Compose аналог — Modifier.semantics { mergeDescendants = true } на контейнере: дочерние описания объединяются в одно объявление. И правило из официальных принципов доступности: когда элементы сгруппированы, интерактивным делают только родителя, а не каждого ребёнка.

Ещё два механизма, которые любят спрашивать на собеседовании:

  • Заголовки. Пользователь TalkBack может навигировать по заголовкам, как по оглавлению. В Views — android:accessibilityHeading="true", в Compose — Modifier.semantics { heading() }.
  • Pane titles. На Android 9 (API 28+) экран можно разбить на логические «панели» (например, корзина и каталог) и дать каждой название через android:accessibilityPaneTitle (в Compose — paneTitle в семантике). Тогда сервис понимает, что произошло изменение в конкретной панели.

Как отвечать про фокус

Ключевое слово — «семантика». TalkBack не «видит» экран как картинку: он обходит семантическое дерево элементов в порядке фокуса. Наша задача — сделать это дерево осмысленным: описать элементы, сгруппировать связанные, пометить декоративное, расставить заголовки. Если кандидат говорит это своими словами — тема закрыта.

Контраст, цели касания и не только цвет

Доступность — это не только экранные дикторы. Есть жёсткие визуальные правила, которые проверяются автоматически и которые стоит знать точно.

Контраст текста. Отношение контраста между цветом текста и фоном должно быть минимум 4,5:1 для обычного текста (меньше 18sp, или жирного меньше 14sp) и 3:1 для крупного. Это не «рекомендация дизайнера», а порог из гайдлайнов — его можно проверить любым онлайн-калькулятором или приложением Accessibility Scanner.

Цель касания. Минимальная область касания для интерактивного элемента — 48 × 48 dp. «Больше — лучше», и на маленьком экране легко сделать так, что кнопка визуально 32 dp, а её цель касания 48 dp за счёт невидимой области. В Compose это делается просто:

kotlin
TouchTarget.kt
@Composable
fun RatingIcon(onClick: () -> Unit) {
    Box(
        modifier = Modifier
            .size(48.dp) // минимальная цель касания
            .clip(CircleShape)
            .clickable(onClick = onClick)
    )
}

Не только цвет. Если два состояния элемента отличаются только цветом (активная вкладка, выбранный пункт), пользователь с дальтонизмом их не различит. Гайдлайн гласит: используйте в дополнение к цвету форму, текст, паттерн или тактильный отклик. На собеседовании стоит упомянуть, что вы кодируете состояние не только цветом.

  • Контраст текста: минимум 4,5:1 для обычного, 3:1 для крупного текста.
  • Цель касания: минимум 48 × 48 dp.
  • Состояние элемента нельзя кодировать только цветом — добавьте форму, текст или паттерн.
  • Медиа: у видео нужны субтитры и контролы, у аудио — транскрипт, если контент критичен.

Как проверять доступность

Хорошая новость: многое проверяется автоматически, и это отдельная тема для собеседования. Если кандидат рассказывает, как он проверял доступность, — это сильный сигнал.

  • Accessibility Scanner — приложение Google, которое сканирует открытый экран и находит проблемы: маленькие цели касания, низкий контраст, отсутствующие описания. Запускается поверх любого приложения.
  • Android Lint — часть сборки. Часть accessibility-проблем (например, отсутствие contentDescription у ImageView) ловит статический анализ.
  • TalkBack вручную — включите TalkBack и пройдите весь сценарий жестами с выключенным экраном. Это самый честный тест: вы сразу услышите, где навигация «плывёт».
  • Автотесты — в Compose можно проверять семантику (SemanticsNodeInteraction, утилиты из androidx.compose.ui.test), в Views — accessibility-события через AccessibilityService-тесты.

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

Меня зовут Рустем Бикбулатов, я senior Android-разработчик и ментор Яндекс Практикума, провожу технические собеседования. За время менторства у меня больше 100 учеников и разборов. Тема доступности — из тех, что большинство кандидатов с опытом 1–2 года пропускает целиком, а зря: на собеседовании она отлично отделяет тех, кто «писал экраны», от тех, кто думал о пользователе.

Хотите прокачаться перед собеседованием?

Вопросы про UI, доступность и качество интерфейсов — это не «скучная обязаловка», а способ выделиться на фоне остальных кандидатов. Бесплатная диагностика: разберу ваши пробелы и составлю план роста за 30–40 минут, без обязательств.

Итоги

Доступность в Android — это не набор атрибутов, а система описания интерфейса для accessibility-сервисов. Поймите её, и вопросы «зачем contentDescription» перестанут быть страшными.

  • Сервисы — TalkBack, Switch Access, Voice Access «читают» семантику вашего UI и помогают пользователю. Ваша задача — описать элементы, а не «нарисовать» их.
  • ОписанияcontentDescription передаёт назначение, а не внешний вид; декоративное помечается как невидимое; описания уникальны.
  • Навигация — фокус движется по семантическому дереву: группы (screenReaderFocusable/mergeDescendants), заголовки, pane titles.
  • Визуальные правила — контраст 4,5:1, цель касания 48×48 dp, состояние не только цветом.

Что запомнить

Одна фраза для собеседования: «Доступность — это качество семантики UI: каждый элемент описан, сгруппирован и логически упорядочен, чтобы TalkBack мог озвучить и выполнить сценарий без зрительного контакта». Добавьте пример проверки через Accessibility Scanner и TalkBack — и ответ станет запоминающимся.

Если вы узнали себя в «ставил contentDescription, чтобы линт молчал» — это нормальная точка роста. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.