Реалізація підтримки TalkBack для Android-додатків
TalkBack — screen reader для Android, аналог iOS VoiceOver. Включається через Settings → Accessibility → TalkBack або довгим нажатям Volume Up + Volume Down на більшості пристроїв. При включеному TalkBack одиночний тап — фокус та озвучування елемента, подвійний тап — активація. Свайп вправо/вліво — перехід між елементами.
Що ламається без розробки
ContentDescription та ImportantForAccessibility
android:contentDescription — еквівалент accessibilityLabel на iOS. Для ImageView, ImageButton — обов'язковий. Для TextView TalkBack читає text автоматично. Декоративні зображення: android:importantForAccessibility="no" (XML) або ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO) — TalkBack пропускає.
Часта помилка в Compose: Icon(painter = painterResource(R.drawable.ic_close), contentDescription = null) — іконка недосяжна для TalkBack, якщо не завернута в кликабельний елемент з явним описом. IconButton з contentDescription у Icon — правильно.
Групування елементів
ViewGroup з android:focusable="true" та android:importantForAccessibility="yes" — TalkBack читає всі дочірні елементи як один: contentDescription контейнера. Для карточки товару (зображення + назва + ціна + кнопка) — вигідніше зробити карточку одним accessible елементом з складним contentDescription через ViewCompat.setAccessibilityDelegate.
У Jetpack Compose: Modifier.semantics(mergeDescendants = true) { contentDescription = "Товар: $name, ціна: $price" } — об'єднує всі дочірні елементи в один для TalkBack.
AccessibilityDelegate та кастомні дії
TalkBack за замовчуванням оголошує стандартні дії: «Double-tap to activate». Кастомні дії (свайп по карточці → видалити, довге нажаття → меню) потрібно реєструвати явно:
ViewCompat.setAccessibilityDelegate(cardView, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View, info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.addAction(
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_DISMISS,
"Видалити зі списку"
)
)
}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS) {
removeItem()
return true
}
return super.performAccessibilityAction(host, action, args)
}
})
Live Regions для динамічного контенту
Лічильник корзини, таймер зворотного відліку, статус завантаження — контент змінюється без дії користувача. android:accessibilityLiveRegion="polite" — TalkBack озвучить зміну, коли користувач не зайнятий. "assertive" — перебиває поточне озвучування. У Compose: Modifier.semantics { liveRegion = LiveRegionMode.Polite }.
RecyclerView та фокус
TalkBack лінійно обходить RecyclerView по елементах. Якщо RecyclerView вкладений в ScrollView — фокус може застрягти. RecyclerView не повинен бути вкладений в ScrollView: стандартна рекомендація Material Design і вона ж необхідна для доступності.
Елемент RecyclerView з кількома кликабельними зонами (наприклад, карточка + кнопка «Додати» всередину): переконатися, що TalkBack фокусується на кожній зоні окремо, а не лише на кореневому view. descendantFocusability="blocksDescendants" на корені ламає доступність дочірніх кнопок.
Процес аудиту
Включаємо TalkBack на реальному пристрої (не емулятор — Samsung One UI та stock Android ведуть себе по-різному). Проходимо основні user flow. Фіксуємо: елементи без опису, недосяжні зони, неправильний порядок фокуса, відсутність live regions для динамічних даних.
Інструменти: Accessibility Scanner (Google Play) — візуально підсвічує проблеми прямо поверх додатка. Android Studio Layout Inspector з Accessibility tab. Espresso з AccessibilityChecks для автоматизованого регресійного тестування:
@Before
fun setUp() {
AccessibilityChecks.enable().setRunChecksFromRootView(true)
}
Термін: 3-5 днів. На Samsung пристроях потрібна додаткова перевірка — One UI добавляє поверх TalkBack власні жести, які можуть конфліктувати з кастомними gesture recognizer'ами.







