Irbisa · Шпаргалка
Доступность и локализация в Android
TalkBack, contentDescription, semantics в Compose, plurals, язык приложения, RTL, масштаб шрифта
Что входит в доступность Android-приложения?
Что такое
contentDescriptionи как подписать кнопку-иконку в Compose и во View?// Wrong: no label at all. TalkBack focuses it and says just "button".IconButton(onClick = ::share) { Icon(Icons.Default.Share, contentDescription = null)} // Right: IconButton merges its children, so the icon's label becomes the button's…Как локализовать Android-приложение и что в нём должно быть с самого начала, чтобы его можно было перевести?
Как скрыть от TalkBack декоративный элемент или целое поддерево в Compose и во View?
// Decoration in Compose: no description means no node, so TalkBack skips itImage(painterResource(R.drawable.header_gradient), contentDescription = null) // Hide one node from services but keep it addressable in testsText("3", Modifier.semantics { hideFromAccessibility() }.testTag("unreadBadge"))…Каким должен быть минимальный размер зоны касания и контраст в Android UI и как получить 48dp, не перерисовывая иконку?
// Modifier order decides the target: padding AFTER clickable is inside itIcon( Icons.Default.Close, contentDescription = stringResource(R.string.close), modifier = Modifier .clickable(onClick = onClose)…Что в Compose меняют в речи TalkBack
mergeDescendants,clearAndSetSemantics,stateDescriptionиrole?// literals for brevity; use stringResource in real code// One stop, one sentence instead of five starsRow( Modifier.semantics(mergeDescendants = true) { contentDescription = "Rating: 4 out of 5" },…Что TalkBack озвучит для этой строки настроек и как это исправить?
@Composablefun WifiRow(checked: Boolean, onToggle: (Boolean) -> Unit) { Row( Modifier .fillMaxWidth() .clickable { onToggle(!checked) } .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text("Wi-Fi", Modifier.weight(1f)) Switch(checked = checked, onCheckedChange = onToggle) }}Как сделать удаление свайпом и перетаскивание доступными для пользователей TalkBack и Switch Access?
// Views: actions the user reaches from TalkBack's actions menuViewCompat.addAccessibilityAction(holder.itemView, getString(R.string.delete)) { _, _ -> onDelete(item.id); true}ViewCompat.addAccessibilityAction(holder.itemView, getString(R.string.move_up)) { _, _ -> onMove(item.id, offset = -1); true…Как сообщить пользователю TalkBack об изменении, например о завершении загрузки, и почему
announceForAccessibilityобъявлен устаревшим?// Deprecated in Android 16 (API 36): interrupts and leaves nothing to go back tostatusView.announceForAccessibility(getString(R.string.upload_finished)) // A live region speaks when its own text changes; focus stays where it wasViewCompat.setAccessibilityLiveRegion( binding.status,…Открывается самописная шторка фильтров, TalkBack продолжает читать список под ней, а после закрытия фокус прыгает наверх. Как это исправить?
// A real modal window hides what is behind it and is announced as a new pane.// On dismiss, hand focus back to the control that opened it.var open by remember { mutableStateOf(false) }val filterButton = remember { FocusRequester() }IconButton(onClick = { open = true }, modifier = Modifier.focusRequester(filterButton)) { Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.filters))…Почему размер текста задают в
spи что изменило нелинейное масштабирование шрифта в Android 14?// Wrong: text sized in dp, in a box that cannot growRow(Modifier.fillMaxWidth().height(56.dp)) { // clips at 200% Text( item.title, fontSize = with(LocalDensity.current) { 16.dp.toSp() }, // cancels the font scale maxLines = 1,…Как тестировать доступность на Android и чего не видят Accessibility Scanner и автоматические проверки?
// View tests: one setup call turns ATF on for every Espresso interaction@Before fun enableChecks() { AccessibilityChecks.enable() .setRunChecksFromRootView(true) // the whole screen, not just the view .setSuppressingResultMatcher( // carry one tracked failure matchesViews(withId(R.id.legacy_banner)),…Найдите баг: кнопка лайка работает от пальца, но когда пользователь TalkBack делает на ней двойное касание, ничего не происходит.
// Like button: shrinks while pressed, toggles the like on releasebinding.likeButton.setOnTouchListener { view, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> view.animate().scaleX(0.85f).scaleY(0.85f) MotionEvent.ACTION_UP -> { view.animate().scaleX(1f).scaleY(1f) viewModel.toggleLike(post.id) } MotionEvent.ACTION_CANCEL -> view.animate().scaleX(1f).scaleY(1f) } true}Откуда TalkBack берёт дерево узлов и как открыть для него части графика, нарисованного на Canvas?
// Ten bars on one canvas are ONE node until you publish virtual onesclass ChartTouchHelper(private val view: ChartView) : ExploreByTouchHelper(view) { override fun getVirtualViewAt(x: Float, y: Float): Int = view.barIndexAt(x, y) ?: HOST_ID…Что не так с этими строковыми ресурсами, когда приложение выходит на русском, и как это исправить?
<!-- res/values/strings.xml --><string name="cart_count">%d items in cart</string><string name="moved">Moved %s to %s</string> <!-- res/values-ru/strings.xml --><string name="cart_count">%d товаров в корзине</string><string name="moved">В папку %s перемещён файл %s</string> <!-- call sites getString(R.string.cart_count, count) getString(R.string.moved, file.name, folder.name)-->В
values/у вас английский, естьvalues-ru; в списке языков пользователя украинский, затем русский. Какие строки он увидит и почему?Как реализовать переключатель языка внутри приложения через per-app language на Android 13+ и на старых версиях?
// res/xml/locales_config.xml lists the shipped locales and is referenced by// <application android:localeConfig="@xml/locales_config">,// or generated by AGP: androidResources { generateLocaleConfig = true } // One call, every API level. 33+ hands it to LocaleManager and recreates the// activities; below 33 AppCompat stores and re-applies it (autoStoreLocales).…Найдите баг: поиск мест рядом работает на телефонах с английским языком, но ничего не находит, если язык телефона русский или немецкий.
class PlacesRepository(private val http: HttpClient) { suspend fun nearby(lat: Double, lon: Double, radiusKm: Double): List<Place> { val url = "https://api.example.com/places?lat=%.6f&lon=%.6f&radius=%.1f" .format(lat, lon, radiusKm) return http.get(url).body() }}Как поддержать языки с письмом справа налево и что Android не отзеркалит за вас?
// Mirrors for free: start/end everywhere, never left/rightRow(Modifier.padding(start = 16.dp, end = 8.dp)) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) Text(order.title, Modifier.weight(1f))}…Как форматировать даты, числа и валюты под локаль пользователя и что ломается с самописными паттернами и
toDouble()?// Wrong: three bugs in three linesval date = SimpleDateFormat("dd/MM/yyyy").format(order.date) // hand-made patternval total = "$" + String.format("%.2f", order.amount) // always dollarsval parsed = input.toDouble() // throws on "1.234,50" // Right: the locale formats; the data decides what is being formatted…Как выстроить процесс локализации, чтобы переводы, пришедшие за несколько дней до релиза, ничего не сломали?