Talkback на custom view: заставляем Android читать что угодно

Android Talkback свалился на меня неожиданно. Сказал бы даже — я совершенно внезапно узнал о его существовании.

Вроде бы совсем недавно всех устраивал один из кастомных контролов в нашем Android-приложении (унаследованный напрямую от View и рисующийся, соответственно, вручную), как вдруг однажды эта идиллическая картина была единым махом разбита в прах жалобой от незрячего пользователя: у него не работает Talkback.

Одно движение — и Talkback включится
Одно движение — и Talkback включится

Откровенно говоря, слово «Talkback» я узнал только из багтрекера, читая задачу «срочно починить!». Потому что если человек может видеть хотя бы в очках, то никогда не включит эту функцию и даже не задумается о ней. Talkback — инструмент Android Accessibility, средство для чтения с экрана, позволяющее тыкать пальцами в самом прямом смысле вслепую. Механический голос вполне нормально зачитывает текст со встроенных в систему контролов (Button, TextView, WebView и т.д.), но вполне естественно, что полностью кастомный View ему представляется чёрной дырой.

Дыра, не дыра, а задача поставлена. Потому я полез читать документацию, и несколько приуныл: что на Google, что на StackOverflow — везде бодро рекомендуют либо не выпендриваться и пользовать стандартные вьюхи, либо задавать правильный contentDescription. Первое нам никак не подходило, но и второе тоже прошло мимо кассы: у нас рисуется несколько активных областей, каждую из которых нужно озвучивать отдельно. А здесь уже  мешалась специфика работы Talkback: в момент озвучивания текста он запоминает точку нажатия, и далее все touch-события пойдут именно в неё, куда бы там повторно ни ткнул пользователь.

Пришлось сесть и решить проблему.

Вводная для зрячих

Человек, впервые включивший Talkback, зачастую теряется в этом своеобразном режиме. Потому сначала — пара советов, как же всем этим пользоваться.

Итак, при включении Talkback…

  • Одиночное нажатие приводит к озвучиванию текста. Причём можно вести пальцем по экрану — текст будет озвучиваться по мере попадания пальца в новые View. Да, привычные скролл и клик исчезают как класс.
  • Двойной тап производит клик в прошлой озвученной точке экрана. Вы можете нажать на кнопку, выслушать её подпись, а потом дважды ткнуть совсем в другой области — неважно, всё равно это будет клик на кнопку, причём точно в месте первого её касания.
  • Прокрутка и перетаскивание производятся двумя пальцами. Потомки AdapterView при этом звенят как ксилофон, меняя тон по мере движения. И панельку сверху тоже нужно вытаскивать эдаким старообрядческим жестом. Кстати, вы знали что если вытащить её таким образом в обычном режиме, то откроются настройки? Прокрутка двумя пальцами превращается в прокрутку тремя. И, видимо, далее по аналогии.

За остальными тонкостями можно обратиться к справке Talkback, нам сейчас эти возможности не слишком интересны. Главное — выше дан ответ на вопрос, как же эту чОртову бубнилку выключить обратно!

Не делайте так! Всё поправимо!
Не делайте так! Всё поправимо!

Ну а мы переходим далее.

Как обозначить области на View для Talkback?

В общем-то, в теории, всё не особо сложно. Однако же — неочевидно!

Первым порывом всех разработчиков становится отлов accessibility events, проверка координат и, например, подмена contentDescription. Но не тут-то было! Accessibility не даёт никаких данных о координатах нажатий, и хитрый план проваливается ещё на этапе подготовки.

Разгадка кроется в другом: accessibility поддерживает создание внутри нашего контрола виртуальных view, которые и озвучивает. Ну а указание всех таких областей — уже наша работа. Равно как и актуализация их описания.

Если заглянуть под капот системы, то речь идёт о правильном заполнении AccessibilityNodeInfo в методе View.onInitializeAccessibilityNodeInfo(). При этом для начала мы должны создать AccessibilityNodeProvider в методе View.getAccessibilityNodeProvider(), где и задать наши виртуальные области. Но пока не спешите закрывать вкладку с этой статьёй!

Android Accessibility сделали настолько мудрёной вещью, что все черти в аду переломают там все ноги. Разобраться в тамошних бредовых сплетениях и перефутболиваниях событий не могут даже разработчики Google, доказательством чего служит простой факт: единственный доступный пример кода с Google I/O содержит кучи комментариев в духе «никогда не делайте так в реальном проекте!».

Нагло украденная с xda-developers картинка
Нагло украденная с xda-developers картинка

И когда увидел наконец Google что это плохо, то решил проблему привычным всем способом: добавлением нормального человеческого вспомогательного кода в android-support-v4.

Встречайте: ExploreByTouchHelper

Именно задачу предельного упрощения кода и решает класс ExploreByTouchHelper из пакета android.support.v4.widget. И даже он далеко не столь очевиден в использовании, как хотелось бы, равно как обладает большим набором ограничений.

Например, «родные» механизмы accessibility предполагают возможность использования иерархии виртуальных View, здесь же нам доступен только один уровень. Впрочем, это более чем логично, а все потуги родить виртуальный иерархический view внутри иерархических view как-то плоховато пахнут гнилой избыточностью. Несколько хуже обстоят дела с поддержкой коллекций виртуальных view (AccessibilityNodeInfo.CollectionInfo) и диапазонов (RangeInfo), и тут уже придётся погружаться в исходники ExploreByTouchHelper-а, которые выглядят удивительно лаконичными и несложными, но содержат в себе могучую магию и неплохо выносят мозг при попытке охватить всю картину.

Но мы рассмотрим достаточно простую ситуацию. Для демонстрации мной был написан простенький контрол: шахматная доска, которая озвучит незрячему пользователю координаты выбранной клетки (е2-е4 и всё такое прочее). Желающие могут переписать полученный результат на «Морской бой», например — пусть это будет что-то вроде домашнего задания.

ChessboardView.java

Именно такое название дано нашей шахматной доске.

Контрол сделан очень простым: он умеет всего лишь нарисовать саму доску (даже без координатной сетки по краям), обозначить выбранную клетку синеньким, корректно обработать смену состояния… ну и проговорить координаты клетки при включении Talkback!

Партеечку?
Партеечку?

Не вижу смысла разбивать код на части, потому вот он весь, целиком:

Уже по итогам дам несколько замечаний.

Как видите, набор виртуальных клеток создан без лишних премудростей, а id виртуального view совпадает с индексом клетки в массиве. Описание каждой клетки хранится во вспомогательном классе Cell, его же удобно применять при отрисовке доски на экране, сразу храня там координаты и цвет. Разумеется, id не обязаны быть индексами, идти подряд и т.п. — главное чтоб не совпадали с INVALID_ID и были постоянными.

Обратите внимание на перегруженные методы onPopulateEventForVirtualView() и onPopulateNodeForVirtualView(), где мы должны всегда задавать описания переданных нам областей. Также в последнем методе мы указываем, что нас интересует событие клика по виртуальному вью. Кроме того, что очень важно — здесь мы задаём координаты клетки. Хелпер избавляет нас от необходимости явно перечислять всё виртуальные вью, равно как проверять валидность id, что крайне ценно.

В onPerformActionForVirtualView() мы дожидаемся запрошенного ранее клика и даём команду перерисовать виртуальную область (т.е. нарисовать жёлтую рамочку), а затем сообщаем системе, что у нас тут произошёл клик, важный для accessibility. Это и приведёт к озвучиванию области.

Наконец, не забываем установить хелпер как accessibility delegate, и, что очень важно, передать ему обработку hover events! Мы проверяем в dispatchHoverEvent(), а не желает ли хелпер обработать это событие первым, и если нет — вызываем родительский метод.

А в итоге?

Собственно, на этом всё. У нас получилось! Компилируйте ChessboardView (он не зависит ни от каких внешних файлов, всё вшито внутрь), добавляйте на активити, включайте Talkback и наслаждайтесь голосом железной тётки!

И не забывайте о незрячих пользователях. Lint не случайно ругается на отсутствующий contentDescription! Он реально нужен, хотя с первого взгляда представляется чем-то инородным и неиспользуемым.

Пожалуй, продуманный accessibility — ещё один признак качества приложения.

Talkback на custom view: заставляем Android читать что угодно
5, голосов: 1


Комментарии:

Один комментарий на «“Talkback на custom view: заставляем Android читать что угодно”»

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Капча (решите пример) *