Адаптируем Android приложение для незрячих людей. Часть 1: основы
В один из зимних вечеров я сидел дома, читал замечательную книжку «Android-программирование для профессионалов» и наткнулся на главу про Accessibility.
Раньше я об этой теме не задумывался: клал null в поле contentDescription и жил себе спокойно. Но когда получил поверхностные знания, как слабовидящие люди пользуются смартфоном через утилиту TalkBack, у меня появилось желание погрузиться в тему доступности глубже.
Меня зовут Тимур Задворнов, я Android-разработчик в Surf. Расскажу и покажу, как работает Accessibility в Android. Посмотрим, легко ли адаптировать приложения для людей с ограниченными возможностями — или это слишком трудоёмкая задача, к которой даже не стоит подступаться.

Погружаться в тему Accessibility я начал с гуглежа статистики. По данным ВОЗ, в мире более миллиарда человек обладает с той или иной формой инвалидности. «Немало», — подумал я и сразу задался другим вопросом: а почему, если в мире так много людей с инвалидностью, огромное количество популярных и не очень приложений не поддерживают приложения для слабовидящих, глухих, людей с заболеваниями опорно-двигательного аппарата? Решил, что надо исправлять это недоразумение и двигать Accessibility в массы.
Для начала разберемся, что вообще такое Accessibility

Accessibility в переводе с английского — «доступность». В контексте программного обеспечения — разработка ПО таким образом, чтобы им могли пользоваться люди с ограниченными возможностями. У термина есть общепринятое сокращение — a11y: между a и y в слове accessibility 11 букв.
Чтобы разобраться, как работает Accessibility, предлагаю задаться вопросом: а как в целом люди пользуются смартфоном? Пользователь взаимодействует с устройством, передавая ему действия: нажатия, свайпы, потряхивания. Смартфон возвращает результат действия, отображая его на экране.
Мы взаимодействуем со смартфоном как привыкли: нажимаем на экран и получаем результат действия. Люди с ограниченными возможностями пользуются смартфоном точно так же, просто они передают и получают информацию со смартфона другими путями: отправляют голосовые команды, команды с другого устройства ввода. Способов много — подход общий.

В Android существуют встроенные Accessibility Services: TalkBack, Switch Access, Voice Access и прочие. О них можно думать как о плагинах для Android, которые помогают людям с ограниченными возможностями пользоваться смартфоном.
Под термином «Accessibility Services» будут подразумеваться перечисленные сервисы, а не Android-сервисы для работы в бэкграунде. Пишу об этом, потому что в Android есть одноименный сервис — AccessibilityService, который работает в бэкграунде с отправляемыми ему AccessibilityEvent.
Какие Accessibility Services существуют
С каждой новой версией Android выкатывает всё больше новых сервисов для пользователей с ограниченными возможностями. Сейчас в Android есть:
- TalkBack — зачитывает информацию о UI-компонентах;
- Switch Access — управление устройством через специальный контроллер;
- Voice Access — голосовое управление смартфоном;
- Select to Speak — зачитывает текстовый контент в выделенной области;
- Extra Dim — настройки яркости дисплея;
- Magnification — экранная лупа;
- Live Captions — экранные субтитры;
- Live Transcribe — перевод речи в текст;
- Sound Amplifier — управление звуком;
- Lookout — распознавание объектов через камеру;
- Action Blocks — создание сценариев использования.
И это ещё, возможно, не всё!
Остановимся подробнее на первых трёх сервисах: TalkBack, Switch Access, Voice Access. Их используют чаще всего: именно с ними придётся сталкиваться разработчику в повседневной работе. Остальные — вспомогательные. Более подробно про них можно посмотреть в официальной документации.
TalkBack
TalkBack — утилита, которая зачитывает информацию с экрана. Помогает слабовидящим людям пользоваться смартфоном.
TalkBack меняет логику управления смартфоном: пользователь выделяет нужный компонент, и система зачитывает содержимое. Компоненты экрана можно выделять либо тапом, свайпами влево и вправо или зажать палец на экране и двигать его — тогда TalkBack будет зачитывать каждый элемент, на который наведён жест. Свайп вправо — выделится следующий элемент на экране, свайп влево — предыдущий.
Все привычные обычному пользователю жесты — назад, свернуть, меню многозадачности — работают, но их нужно воспроизводить двумя пальцами. Все действия и жесты TalkBack описаны в документации.
На каждом смартфоне с Android на борту можно включить TalkBack и пройти туториал по использованию: всё это делается в настройках, пункт Accessibility.
В настройках TalkBack есть меню с настройками для разработчиков. Можно включить субтитры или добавить шорткат быстрого включения и выключения TalkBack — зажать кнопки громкости на несколько секунд. Очень помогает во время разработки.
Switch Access
Switch Access — сервис для управления смартфоном с помощью специального устройства, переключателя.

Принцип взаимодействия со смартфоном схож с принципом работы через TalkBack. На экране выделен компонент, между компонентами можно переключаться. Обычно на переключателе есть несколько кнопок. В качестве переключателя можно использовать специальное устройство или настроить стороннюю клавиатуру, а также кнопки громкости на смартфоне для удобного дебаггинга.
Voice Access
Voice Access — сервис для голосового управления смартфоном. Пользователь говорит, какое действие нужно сделать на экране: нажать кнопку «Лайк», пролистать вниз, ввести текст в поле «Сообщение». Действие выполняется. На видео — Voice Access в действии.
На самом деле Voice Access далеко не идеальная вещь: иногда тупит и некорректно распознаёт слова — особенно названия приложений.
Что происходит со стороны разработчиков
Давайте посмотрим, какие инструменты есть для разработчиков, чтобы UX приложений не страдал. Хорошие новости: все компоненты из стандартной библиотеки Android по умолчанию поддерживают Accessibility. Не придётся с нуля разбираться и делать доступными базовые компоненты: TextView, EditText, Switch и другие.
Приложение Accessibility Scanner для помощи разработчикам
Google выпустил для разработчиков приложение Accessibility Scanner: оно помогает найти некомфортные для людей с ограниченными возможностями места в приложении.
Accessibility Scanner работает просто:

- Открываем, даём все разрешения, оно отображается над всеми приложениями как плавающая кнопка.
- Заходим в приложение, которое нужно проверить, переходим на нужный экран, нажимаем на кнопку Snapshot.
- Ждём, пока Accessibility Scanner проанализирует экран, и смотрим на результат.
Accessibility Scanner — отличная штука, но у него есть существенный недостаток. Сканер находит только очевидные проблемы: размер текста, размер кликабельной области компонента, отсутствие лейбла у изображения и так далее. В более сложных случаях он бесполезен. Например, не поможет с распознаванием кастомных вью и правильной интерпретацией для слабовидящих: что зачитывать с этого компонента, какие действия с ним можно совершить.
Давайте разберемся, как сделать приложение более доступным с помощью нескольких простых шагов: они закроют процентов 90 всевозможных кейсов.
Шрифты
Правило простое: всегда указывайте размер шрифтов в sp! Вы меня спросите: «Зачем для шрифтов придумали какую-то отдельную единицу? Если я укажу размер в dp, никто не умрёт».

Если пользователь увеличит шрифты в системе, нужно, чтобы они увеличились и в приложении. Для этого и пригодится sp-юнит: если указать размер текста 16sp, а пользователь в системе поставил коэффициент увеличения текста = 1,25, то размер шрифта станет 20dp. На иллюстрациях видно, как изменяется размер шрифта, указанный в sp.

Заголовки
TalkBack предоставляет удобную навигацию между заголовками. Если на экране много текста с заголовками или, например, на экране есть список с подсписками, которые также помечены заголовками, в эти TextView дополнительно следует положить true в поле accessibilityHeading . Тогда текст будет считываться как заголовок.
Этот параметр появился в Android 9. Если нужно использовать его на более старых версиях, эту проблему решает compat-версия этого параметра — ViewCompat.setAccessibilityHeading(view, boolean) .
Контраст
Люди с нарушениями зрения могут воспринимать цвета по-другому: стоит следить, все ли элементы на экране чётко различимы между собой, есть ли цветовой контраст. По гайдлайнам Android, хорошая практика — устанавливать цветовой контраст на наложенных друг на друга компонентах с соотношением более чем 3:1 для больших текстов и 4,5:1 для остального контента.
Что это за цифры? Это соотношение между цветами, которое показывает, насколько сильно контрастируют два цвета между собой. Чтобы было понятно, разберем на примере: соотношение белого цвета (#FFFFFF) к чёрному (#000000) равняется 21:1 — это максимальное возможное соотношение. Два одинаковых цвета имеют отношение 1:1. Проверить контраст между цветами можно на сайте WebAIM.

КАПС — НЕ НАШ БРО
На некоторых девайсах TalkBack читает текст, написанный заглавными буквами, как аббревиатуру – по букве. Сами понимаете, воспринимать это на слух очень трудно.
Заглавными буквами следует писать только аббревиатуры. Основной текст — строчными буквами или sentence case. Если всё-таки нужно оставить текст заглавными буквами, используйте параметр android:textAllCaps=”true” — и тогда всё будет ок.
Размеры элементов
Любые кликабельные элементы на экране должны иметь размер как минимум 48x48dp. Такой размер рекомендован Google: область нажатия пальцем на экран равняется приблизительно этому значению. В вебе, например, рекомендованный размер 44x44px.
Content Description
Не будем лукавить: все мы когда-то устанавливали поле contentDescription в null , потому что «да зачем оно мне надо». Но если вы хотите, чтобы приложение было доступным, придётся разбираться, что это за поле за такое.
Поле contentDescription используется в основном для визуальных компонентов без текста — ImageButton , ImageView и так далее. Это нужно, чтобы TalkBack при наведении на этот элемент не зачитал «Unlabeled, Button, double-tap to activate», а дал конкретную информацию: что за элемент, зачем он нужен на экране и что произойдет, если нажать на него. Эта строка обязательно должна быть локализована: если пользователь из Франции запустит TalkBack, а ему на чистом японском зачитают «дескрипшен», вряд ли кому-то от этого станет легче. И вообще: любые строки, связанные с a11y, будь то contentDescription или дополнительные действия TalkBack, о которых мы поговорим далее, должны быть локализованы.
В этой статье я буду использовать английскую версию TalkBack и локализованные строки так же на английском языке. Не беспокойтесь: на русском TalkBack тоже работает.
Если ImageView имеет на экране чисто декоративную роль, contentDescription можно не указывать. Тогда важно указать другой параметр — importantForAccessibility . Он отвечает за необходимость прочтения TalkBack содержимого этого компонента. У этого параметра есть значения:
- yes — компонент обязателен для прочтения TalkBack.
- no — компонент пропускается.
- noHideDescendants — компонент и его дочерние компоненты пропускаются.
- auto — система сама определяет, обязателен ли компонент для TalkBack. Это значение по умолчанию.
Live Regions
Иногда нужно при действии с одним компонентом на экране обновить значение компонента, не находящегося в фокусе. Для этого у View в Android есть поле accessibilityLiveRegion , которое позволяет зачитать значение из вью, если оно было изменено. У accessibilityLiveRegion есть три возможных значения:
- none — ничего не обновлять, значение по умолчанию.
- polite — если значение обновилось, но TalkBack ещё воспроизводит старое значение, новое значение зачитается после того, как TalkBack закончит говорить.
- assertive — противоположное значению polite . При обновлении значения TalkBack перестанет зачитывать старое значение и сразу начнёт зачитывать новое.
Проще понять на примере: у нас есть кнопка Increment, которая увеличивает значение на 1 и отображает новое значение в TextView. Изменяем значение accessibilityLiveRegion у TextView на polite и вот какой будет результат.
Порядок навигации между View
В идеале порядок должен идти слева направо, сверху вниз, но иногда этот порядок приходится менять для более удобной навигации между компонентами. На картинке ниже — как раз такой пример. Он надуманный, это можно сделать гораздо лучше: объединить заголовок и значение или использовать параметр labelFor .

На этом примере хотелось бы, чтобы после чтения заголовка читалось значение, для которого предназначен этот заголовок. Чтобы изменять порядок навигации, у всех View есть поле accessibilityTraversalBefore . В него нужно передать идентификатор компонента, на который мы хотим переключаться после фокуса текущей выделенной вью.
Также у View есть противоположный параметр — accessibilityTraversalAfter . В нём лежит идентификатор компонента, после которого текущий компонент будет выделен TalkBack. Иногда случается, что нужно задать параметр динамически через код: для этого у объекта класса AccessibilityNodeInfo вызвать метод setTraversalBefore или setTraversalAfter . Подробнее про AccessibilityNodeInfo и AccessibilityDelegate расписано далее.
Давайте укажем к каждому TextView с заголовком параметр android:accessibilityTraversalBefore=»@id/value_textview» , где @id/value_textview — идентификатор соответствующего заголовка TextView со значением. Получим следующий порядок навигации:

Кастомные View
Обычно кастомные вью состоят из нескольких более простых компонентов: текстов, кнопок, свитчеров. Хорошая практика — делать так, чтобы компонент выделялся TalkBack как единый, а не выделял отдельно компоненты-составные, и предлагал какие-то действия что можно сделать с этим сложным компонентом.
Понятнее будет, если разобраться на примерах. Предположим, есть компонент деталей криптокошелька. Возможные составляющие: название, баланс, адрес, кнопки пополнить и перевести.

По умолчанию, если ничего не делать, TalkBack просто зачитает текстовое содержимое компонента: название кошелька, баланс и адрес.

Как можно улучшить UX для незрячих? Сделаем компонент более доступным за несколько простых шагов.
1. Все дочерние элементы пометить importantForAccessibility = no , чтобы TalkBack не читал ничего, кроме кнопок «пополнить» и «перевести». С ними разберёмся позже.
2. Даём корневому элементу понятный contentDescription . Нужно определить, какая информация полезна для прочтения TalkBack. Пользователю будет полезно знать название кошелька и баланс. Обновлять contentDescription будем в функции setData , которая вызывается при инициализации компонента.
fun setData( title: String, balance: String, address: String ) = with(binding) < titleTv.text = title balanceTv.text = balance addressTv.text = address // Строка со значением "Wallet card: %s, balance is %s" root.contentDescription = context.getString( R.string.wallet_card_accessibility_description, title, balance ) >
3. Обрабатываем взаимодействие с компонентом. Какой смысл этого компонента? Что он делает? Выделим список возможных действий:
- По нажатию на карточку переходим на экран деталей по кошельку.
- По нажатию на кнопку «Перевести» или «Пополнить» переходим на экран переводов. С кнопками делать ничего не нужно, мы их не помечали как игнорируемые и они по умолчанию поддерживают accessibility.
- Возможность скопировать адрес кошелька.
- Возможность отобразить адрес в формате QR-кода.
Для перехода к деталям кошелька почти ничего не требуется, если уже назначен слушатель на событие нажатия на карточку. Единственное, что стоит переопределить, — название действия. По умолчанию при нажатии на кликабельный элемент TalkBack будет зачитывать «Double tap to activate» Это понятно, если элемент — кнопка или свитчер. Но если компонент сложный, «activate» не передаёт нужного контекста. Вот бы было можно заменить описание действия, дав больше контекста. И — сюрприз — так можно!
Каждая вью содержит сущность AccessibilityDelegate . Согласно документации, делегат позволяет делать компоненты доступными не с помощью наследования, а с помощью композиции. Он может перехватывать события с UI, создавать кастомные экшны, редактировать существующие — и ещё много вещей, которые прокачают доступность компонентов.
Давайте создадим делегата для нашей карточки. Будем использовать класс ViewCompat для обратной совместимости. Всё это опишем в методе setupAccessibility , который также вызываем при инициализации компонента.
ViewCompat.setAccessibilityDelegate( binding.root, object : AccessibilityDelegateCompat() < override fun onInitializeAccessibilityNodeInfo( host: View, info: AccessibilityNodeInfoCompat ) < super.onInitializeAccessibilityNodeInfo(host, info) val customAction = AccessibilityNodeInfoCompat.AccessibilityActionCompat( AccessibilityNodeInfoCompat.ACTION_CLICK, // Строка со значением "View wallet details" context.getString(R.string.wallet_card_accessibility_card_click_action) ) info.addAction(customAction) >> )
Вы великолепны. Теперь TalkBack при наведении на карточку зачитает действие как «Double tap to view wallet details».
Разберёмся с функциональностью, связанной с адресом. Для компонента помимо основных действий — нажатия, долгого нажатия и так далее — можно добавлять кастомные действия и вызывать их через меню TalkBack. Чтобы открыть меню, нужно при включенном TalkBack сделать L-образный свайп сверху вниз или свайп тремя пальцами вниз — если версия TalkBack выше 9.1. Для добавления кастомного действия используются:
- Функция ViewCompat.addAccessibilityAction , куда передаём корневую вью и название действия.
- Класс AccessibilityViewCommand , в которое кладём код с действием, — просто лямбда.
private fun setupAccessibility() < . . . ViewCompat.addAccessibilityAction( binding.root, // Строка "Show address QR code" context.getString(R.string.wallet_card_accessibility_show_qr_code_action) ) < _, _ ->// Открываем диалог с QR здесь Toast.makeText(context, "QR code has shown", Toast.LENGTH_SHORT).show() true > >
По такой же логике добавляем второе действие — копирование адреса кошелька.
private fun setupAccessibility() < . . . ViewCompat.addAccessibilityAction( binding.root, // Строка "Copy the wallet address" context.getString(R.string.wallet_card_accessibility_copy_the_address_action) ) < _, _ ->// Кладём строку с адресом в буфер обмена Toast.makeText(context, "Address has copied", Toast.LENGTH_SHORT).show() true > >
Кастомная вью теперь гораздо доступнее, чем была до этого. Вот так это будет выглядеть со стороны пользователя.
Можно и дальше придумывать, как сделать этот компонент более доступным. Я разобрал базовые случаи и инструменты для работы с Accessibility. Например, ещё придумал, что можно добавить кастомный экшн на чтение адреса полностью или первых и последних пяти символов.
Если получается много однотипного кода, можно вынести повторяющиеся вещи в функции-расширения. Кастомные действия также подходят для более сложных случаев взаимодействия с UI: перетаскивание элементов в списке, swipe-to-delete и прочие.
Возьмём другой пример. Пойдём дальше по тематике приложения с криптовалютами. У нас есть такой вот глупенький кастомный свитч, у которого есть 2 состояния: пополнить или перевести. Внутри это просто LinearLayout с двумя кнопками.

По смыслу это один компонент, но если ничего не делать, TalkBack будет читать эти две кнопки как отдельные компоненты. Чтобы сделать свитчер более доступным, нужно следующее:
1. Дать осмысленное название свитчу с помощью параметра contentDescription . В этот раз мы можем сделать это прямо в XML, потому что контент вью неизменный, в отличие от прошлой вью — там у нас на входе были название кошелька и баланс.
2. Все кнопки пометить importantForAccessibility = no , чтобы TalkBack не выделял их.
3. У этого компонента два состояния: пополнить или перевести. По смыслу эта кастомная вью — свитчер, но TalkBack про это ничего не знает. Чтобы добавить состояние для компонента, есть поле stateDescription . А зачем оно нужно? Мы же просто можем класть состояние в contentDescription и эффект будет тот же.

Каждый раз, когда мы обновляем состояние компонента и, следовательно, обновляем contentDescription , TalkBack будет его зачитывать.
В contentDescription , помимо состояния, есть ещё описание самого компонента. Чтобы его не повторять, а воспроизводить только изменённое состояние, это состояние выделили в отдельное поле. Хорошая практика: в contentDescription класть только описание компонента — «Operation type». Состояние класть в stateDescription — «Deposit» или «Transfer». Давайте обновлять у нашего свитчера состояние при изменении:
depositButton.setOnClickListener < depositButton.select() transferButton.deselect() updateStateDescription() >transferButton.setOnClickListener
private fun updateStateDescription() < ViewCompat.setStateDescription( binding.root, context.getString( if (binding.transferButton.isSelected) < R.string.operation_switch_transfer_label >else < R.string.operation_switch_deposit_label >) ) >
4. Помимо состояния, вью обозначает, что за компонент и какую роль он играет на экране: кнопки, переключателя, чекбокса, меню-бара и так далее. Можно просто закинуть эту роль в contentDescription — но, конечно, это плохая практика.

Есть два способа обозначить роли: указать className или использовать поле roleDescription . В чём их отличие? Поле className следует перегружать, если кастомный компонент имеет роль стандартного компонента: Button, Checkbox, RadioButton — просто его вёрстка и реализация отличаются.
Если компонент уникальный и у него нет аналогов в библиотеке стандартных компонентов, можно явно указать его название его название с помощью параметра roleDescription . Такими компонентами могут быть рекламные баннеры или кастомные меню-панели. В нашем случае мы создаём аналог Switch, поэтому будем использовать установку роли через className .
ViewCompat.setAccessibilityDelegate( root, object : AccessibilityDelegateCompat() < override fun onInitializeAccessibilityNodeInfo( host: View, info: AccessibilityNodeInfoCompat ) < super.onInitializeAccessibilityNodeInfo(host, info) info.className = Switch::class.java.name // Если вам нужно указать отличную от стандартной роль info.roleDescription = context.getString(R.string.role_description_switch) >> )
5. Мы хотим, чтобы при двойном нажатии на вью при включенном TalkBack свитчер менял своё значение. Этого мы можем добиться с помощью функции ViewCompat.replaceAccessibilityAction . Она переопределяет поведение переданного в функцию экшна, в нашем случае ACTION_CLICK на поведение, переданное в лямбде.
ViewCompat.replaceAccessibilityAction( root, AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, // Строка "Toggle" context.getString(R.string.operation_switch_accessibility_toggle_action) ) < _, _ ->if (transferButton.isSelected) < depositButton.performClick() >else < transferButton.performClick() >true >
Всё! Что у нас получилось в итоге, можно посмотреть на видео ниже. Все примеры кода лежат у меня на GitHub.
Accessibility проще, чем кажется
- Всё не так сложно, как казалось на первый взгляд. Не нужно создавать отдельную версию приложения для людей с ограниченными возможностями.
- Стоит чаще запускать Accessibility Scanner и исправлять ошибки. Не забываем про указание заголовков для удобной навигации, не кладём null в contentDescription , следим за порядком навигации между компонентами и делаем кастомные вью доступными!
- Все строки, связанные с a11y, следует выносить в strings.xml и делать для них локализацию для языков, которые поддерживаются приложением.
- Все стандартные компоненты Android SDK по умолчанию поддерживают a11y.
- Во время тестирования приложения попробуйте попользоваться им, не подглядывая на экран, — только через TalkBack. Может быть, вы найдете моменты, которые можно было бы сделать лучше.
В поддержке Accessibility одни плюсы: люди с ограниченными возможностями смогут пользоваться приложениями без неудобств, в то же время в большинстве случаев у разработчиков поддержка не занимает много времени и ресурсов. Правда, порой встречаются очень необычные кейсы, где поддержать a11y — это вызов для всей команды разработки. Примерами могут послужить сложные кастомные вью, нарисованные на Canvas, большие таблицы с огромным количеством данных или графики.
Больше полезного про Android — в нашем телеграм-канале Surf Android Team. Здесь мы публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!
Больше кейсов команды Surf ищите на сайте >>
- surf
- accessibility
- доступность
- мобильная разработка
- разработка под андроид
- разработка под android
- talkback
- разработка мобильных приложений
What is android:contentDescription=»@string/desc» in ImageView’s xml?
I added an imageView in GraphicalLayout but it looks different on my real device than on AVD. I found that I need to add contentDescription in a layout .xml , but when I add: contentDescription=»@string/desc» there is an error:
«No resource found that matches the given name (at ‘contentDescription’ with value ‘@string/desc’)»
What is this string «desc»? What should it looks like?
asked Jul 21, 2012 at 21:05
8,714 6 6 gold badges 33 33 silver badges 33 33 bronze badges
6 Answers 6
It’s for the Accessibility Features of Android. The contentDescription is what get’s read back to the (assumingly blind or hard-of-sight user) so they have an idea of what the picture is since they aren’t able to view it.
As to answer your question, @iturki gives the start of it. You write the string to use for the description in strings.xml and give it a name of ‘desc’ or whatever your heart desires, then you set the value of contentDescription in the ImageView to @string/desc , either in the layout or programmatically. Like I said before, it’s to help users with poor eyesight get a general idea of what the image is portraying 🙂
answered Jul 21, 2012 at 21:12
starscream_disco_party starscream_disco_party
2,846 5 5 gold badges 26 26 silver badges 42 42 bronze badges
It’s not technically an error, it’s a warning saying that the Accessibility isn’t as good as it could be. Adding a string to contentDescription just gives TalkBack , or whichever Accessibility app the user has, content to read back to describe the image, since it’s not physically viewable by the impaired user.
Jul 21, 2012 at 21:27
And for purely decorative images, you can set android:contentDescription=»@null» to no content description.
Как проявляется contentDescription
В учебнике по Android’у несколько раз упоминается про параметр (и метод) contentDescription . К примеру, в разметке я его устанавливал в ImageView :
Но я никак не могу понять для чего это! На картинку и нажимал, и удерживал, но никаких эффектов, текста и тд. Так для чего же этот contentDescription и как он проявляется?
Отслеживать
задан 25 мар 2016 в 23:05
user189127 user189127
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Это для слабовидящих. Текст в значении этого аттрибута будет озвучен системой при нажатии на картинку в случае, если в настройках устройства включена соответствующая опция.
Озвучка идёт чрез гугловое приложение
Отслеживать
ответ дан 25 мар 2016 в 23:12
69.8k 9 9 золотых знаков 66 66 серебряных знаков 122 122 бронзовых знака
Я бы хотел добавить, что это очень важный аттрибут. Я никогда о нем не задумывался пока мне не написал незрячий человек. Пришлось несколько дней отлаживать всю программу, что бы такие люди могли ей пользоваться. Обычно используют TalkBack. Еще такой момент, есть текст в виде html, так вот для зрячих его видно красиво, а вот TalkBack может начать читать тэги и прочую лабуду. Пришлось и в такие компоненты прописывать чистый текст.
Урок 5. Background и border. Изображения.
В этом уроке разбираемся какие возможности у нас есть при создании фона и границ элементов; какие функции используются для вывода на экран изображений; как показать картинку из Интернет.
В прошлых уроках мы уже добавляли фон в некоторые элементы. Мы использовали только цвет, но есть возможность задать и форму. Кроме фона мы также можем настраивать границы элемента.
Рассмотрим эти возможности на простом примере:
import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.unit.dp @Composable fun HomeScreen() < Box(contentAlignment = Center, modifier = Modifier.fillMaxSize()) < Box(modifier = Modifier .width(160.dp).height(120.dp) .background(color = Color.Cyan) ) >>
В центре экрана выводим пустой Box размером 160×120.

На нем и будем тренироваться.
Background
В примере мы в background указали только color. Но там есть еще один параметр — shape. Он имеет значение по умолчанию — RectangleShape. В результате мы получаем обычный прямоугольник.
Давайте используем другую форму, например, закругленные углы — RoundedCornerShape
.background(color = Color.Cyan, shape = RoundedCornerShape(16.dp))

Или срезанные углы — CutCornerShape:
.background(color = Color.Cyan, shape = CutCornerShape(16.dp))

Еще есть круглый фон:
.background(color = Color.Cyan, shape = CircleShape)

Получился не совсем круг, потому что ширина и высота элемента не равны. Но такой результат вполне можно использовать для небольших текстов, которые показывают, например, количество непрочитанных сообщений.
Вернемся к цвету.
Если нам нужен не фиксированный цвет, а что-то посложнее, используем другую версию background, которая принимает на вход Brush. С его помощью можно сделать градиент:
.background( brush = Brush.linearGradient(colors = listOf(Color.Red, Color.Yellow, Color.Green)), shape = RoundedCornerShape(16.dp) )

Также вместе с Brush можно указать и прозрачность:
.background( brush = Brush.linearGradient(colors = listOf(Color.Red, Color.Yellow, Color.Green)), alpha = 0.2f, shape = RoundedCornerShape(16.dp) )

Border
С помощью border мы можем задать как будет выглядеть граница нашего элемента. Уберем пока background, и добавим border:
import androidx.compose.foundation.border @Composable fun HomeScreen() < Box(contentAlignment = Center, modifier = Modifier.fillMaxSize()) < Box(modifier = Modifier .width(160.dp).height(120.dp) .border(width = 2.dp, color = Color.DarkGray) ) >>

Так же, как и у background, у border есть параметр shape, где мы можем указать нужную нам фигуру:
.border(width = 2.dp, color = Color.DarkGray, shape = RoundedCornerShape(16.dp))

Также есть и Brush:
.border(width = 2.dp, brush = Brush.linearGradient(colors = listOf(Color.Red, Color.Yellow, Color.Green)), shape = RoundedCornerShape(16.dp))

Фон и границу можно ставить вместе. При этом можно даже поставить им разные формы
.background(color = Color.Cyan, shape = RoundedCornerShape(16.dp)) .border(width = 2.dp, color = Color.DarkGray, shape = CircleShape)
но результат будет так себе:

Изображения
Для изображений у нас есть два элемента: Image и Icon
import androidx.compose.foundation.Image import androidx.compose.material.Icon import androidx.compose.ui.res.painterResource @Composable fun HomeScreen() < Box(contentAlignment = Center, modifier = Modifier.fillMaxSize()) < Image( painter = painterResource(id = R.drawable.sand), contentDescription = null ) Icon( painter = painterResource(id = R.drawable.ic_launcher_foreground), contentDescription = null ) >>
Оба используют painterResource, чтобы достать картинку из ресурсов

Разница между ними понятна из их названий.
Image — для картинок. У него есть стандартные параметры для настройки положения и отображения картинки: alignment, contentScale, alpha, colorFilter.
Icon — для иконок. У него есть параметр tint, который позволяет задать оттенок иконки.
Для отображения картинок из Интернет можно использовать библиотеку Coil.
implementation 'io.coil-kt:coil-compose:2.2.2'
И используем AsyncImage, который под капотом скачает картинку и отобразит ее на экране.
AsyncImage( model = "https://developer.android.com/images/android-go/next-billion-users_856.png", contentDescription = null )
Не забудьте в манифест добавить Permission на Internet.
Если картинка не грузится, то попробуйте перезапустить или переустановить приложение.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
Комментарии
# RE: Урок 5. Background и border. Изображения. — Максим 19.12.2022 11:00
.border(width = 2.dp, brush = Brush.linearGradient(colors = listOf(Color.Red, Color.Yellow, Color.Green)), shape = RoundedCornerShape(16.dp))
И тут я просто расплакался