Если вы мобильный разработчик, знаете ли вы, каково пользоваться вашим приложением незрячим людям? В каких именно местах они могут столкнуться с главными сложностями? Как вообще будет происходить взаимодействие с приложением?

Виктор Вихров (Яндекс Go) рассказал о том, как сделать Android-приложение более подходящим для использования «вслепую», на нашей конференции Mobius. А эксперт в сфере цифровой доступности Анатолий Попко помог ему, прокомментировав представленные решения с точки зрения такого пользователя. Мы считаем, что такой контент нужен и на Хабре — поэтому сделали текстовую расшифровку доклада.

Специальные возможности

Возможно, не все в курсе, как незрячие люди используют телефоны. В видеозаписи доклада это продемонстрировано на 3:45: 

Пользователи воспринимают весь интерфейс на слух (субтитры на этом видео отображаются только из-за режима разработчика), и очень важно, чтобы элементы управления были понятно озвучены.

Также важно, чтобы пользователь с помощью программы чтения экрана мог перейти на любой экран и сделать всё, что ему нужно. Google предоставляет для этого ряд приложений:

  • TalkBack — используется незрячими пользователями для навигации и озвучивания элементов экрана. 

  • Select to Speak — зачитывает выделенный текст. 

  • Switch Access — управляет устройством при помощи специальных переключателей.

Они нужны для пользователей с проблемами опорно-двигательного аппарата. Одна кнопка позволяет перемещаться по экрану, вторая — совершать какие-то действия. Это может быть не только кнопка, но и любое другое средство управления, которое можно подключить к телефону. 

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

На Android-устройствах обычно предустановлен TalkBack — это разработка Google. Также можно встретить Screen Reader Voice Assistant, который установлен на устройствах Samsung. Также есть множество разных других программ для чтения экрана, потому что любой разработчик может создать и выложить ее в Google Play Store, но это самые популярные. Недавно в Samsung приняли решение, что они дальше будут развивать TalkBack, а Voice Assistant остался только на старых устройствах. 

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

Можно выделить два вида навигации при использовании TalkBack. Первый — это линейная навигация, которая происходит с помощью свайпов влево-вправо, таким образом фокус переключается по элементам. Второй называется Explore by touch — исследовать экран можно просто водя пальцем, и каждый элемент на экране, который оказался под пальцем, будет озвучиваться. 

Чаще всего пользователи выбирают линейную навигацию, чтобы пользоваться новым приложением, которое им неизвестно. Но если они уже давно пользуются приложением и знают расположение элементов, например, что кнопка «Заказать» располагается внизу, то могут быстро перейти к ней. 

TalkBack поддерживает множество жестов:

  • жесты, которые используют MultiTouch;

  • комбинированные жесты: например, вертикальный плюс горизонтальный. 

Ознакомиться с ними со всеми можно в документации.

Очень важная функция для разработчиков, которую можно сразу не заметить, — возможность быстрого включения TalkBack. Когда я только начинал заниматься этим вопросом, я всегда заходил в настройки. Это отнимало много времени, а можно было всё делать быстро. Только если вы включаете TalkBack после того, как зашли в приложение, то могут быть артефакты в поведении, потому что TalkBack недополучил ивенты от приложения. 

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

Наверное, можно подумать, что незрячим пользователям удобнее использовать голосовой ввод, но не все приложения его поддерживают. Скорее он идет как дополнение к вводу текста, поэтому есть адаптированная клавиатура. Она работает так же, то есть по буквам можно переключать фокус и выбирать нужный символ. 

Также есть специальная клавиатура Брайля, которая использует шрифт Брайля, где каждый символ закодирован набором из шести точек. Вот так это выглядит:

Анатолий, может, прокомментируешь, какой из способов наиболее удобен, чем обычно пользуешься?

Анатолий Попко: С недавнего времени TalkBack начал поддерживать то, что называют «Брайлевским вводом». Выглядит довольно странно, но это совершенно гениальная с точки зрения эргономики идея, которая позволяет использовать всю площадь экрана для ввода символов рельефно-точечным шрифтом Брайля. То есть шрифт используется не для чтения, как мы привыкли, а для ввода текста, и этот способ ввода увеличивает в разы то, с какой скоростью незрячий пользователь может набирать текст, причем в самых разных ситуациях: и в метро в наушниках, и где-то на бегу. Я писал таким образом в самых разных местах — получается довольно быстро. 

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

Общие подходы к доступности

Виктор: Теперь перейдем к проблемам, которые могут встретиться пользователям в приложении, и рассмотрим способы их решения. 

Озвучивание элементов 

Вот пример кнопки, она озвучивается как «Меню» —> «Кнопка» —> «Коснитесь дважды, чтобы активировать». 

Чекбокс будет озвучен по-другому: у него будет добавлено состояние «Не отмечено» или «Отмечено». 

Можно выделить некоторые части описания, которые озвучиваются: 

  • описание («Меню, «Комфортное вождение»);

  • тип («Кнопка», «Переключатель», «Флажок»);

  • состояние («Включен», «Отмечен»);

  • подсказка («Коснитесь дважды, чтобы включить или выключить»). 

Они нужны, чтобы пользователь понял, что это за элемент и как с ним взаимодействовать. Пользователь может менять порядок чтения этих частей и настраивать детализацию, то есть не зачитывать некоторые части (например, подсказку). 

Рассмотрим подробно каждый из этих пунктов. 

Описание

Описание берется из текстового контента, если он есть. UI-элементы из Android SDK вроде TextView или Button содержат атрибут text с текстовым контентом, и в них уже прописано, как текст будет отправляться в TalkBack.

Если View-элемент не содержит текстового контента, то мы можем сами указать это с помощью contentDescription. 

Если нет ни того, ни другого, то элемент просто не будет озвучен. Это будет выглядеть как «Без ярлыка» → «Кнопка», то есть текст-заглушка. 

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

Задать описание очень просто — нужно вызвать метод setContentDescription() и передать туда строку с корректным описанием:

menuButton.setContentDescription(
  getString(R.string.menu_button_content_description));
<ImageView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:contentDescription="@string/my_location_description"
  />

Не всегда текстовые поля, которые содержат текстовый контент, доступны. Бывает, что есть дополнительная информация, например, в иконке, для которой тоже нужно задать описание. Если вы задали описание для элемента, то оно будет переопределять текстовый контент внутри него.

Несколько рекомендаций, как лучше давать названия элементам:

  • Не указывайте в названии элемента его тип или состояние. TalkBack получает тип элемента из другой метаинформации, поэтому тут нужно просто описывать назначение, иначе потом тип может быть продублирован. 

    Примеры: «Меню», а не «Кнопка меню»; «Заказать», а не «Кнопка "заказать" отключена».

  • Помещайте изменяемую информацию в начало описания. Если пользователь часто заходит в приложение и уже знает, что, например, у него где-то будет бейджик в виде плюса с его баллами, то ему будет полезно сначала получить информацию о количестве его баллов, а остальную информацию уже можно не дослушивать. 

    Примеры: «777 баллов плюса», а не «Баллов плюса: 777»; «1 активный заказ», а не «Активных заказов: 1».

  • Избегайте слишком подробных описаний, заставляя пользователей слушать их целиком. Давайте четкие и понятные названия, но не в ущерб пониманию. 

    Пример: «Поделиться», а не «Нажмите эту кнопку, чтобы поделиться новостью».

  • Давайте уникальное описание. Если в интерфейсе есть несколько кнопок «изменить что-то» или «удалить что-то», то иногда нужно дать больше контекста и сказать, что мы удаляем: адрес электронной почты из профиля или сам профиль.

Тип 

В классе View есть метод getAccessibilityClassName(), который возвращает строковое название класса.

// класс View

public CharSequence getAccessibilityClassName(){
  return View.class.getName();
}

Соответственно, наследники класса View переопределяют этот метод: 

// класс Button

@Override
public CharSequence getAccessibilityClassName(){
  return Button.class.getName();
}

После чего он некоторым образом доходит до TalkBack, который получает ивенты.

Когда TalkBack определяет тип этого элемента, то просто сопоставляет строковое название класса со всеми известными типами из Android SDK, которые там есть. 

// Inheritance: View->TextView->Button
if (ClassLoadingCache.checkInstanceOf(className,android.widget.Button.class)) {
  return ROLE_BUTTON;
}
						
// Inheritance: View->ImageView
if (ClassLoadingCache.checkInstanceOf(className, android.widget.ImageView.class)) {
  return node.isClickable() ? ROLE_IMAGE_BUTTON : ROLE_IMAGE;
} 

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

public static String roleToSummaryString(@Role.RoleName int role, Context context) { 
  switch (role) {
    case Role.ROLE_BUTTON:
      return context.getString(R.string.value_button);
    case Role.ROLE_CHECK_BOX:
      return context.getString(R.string.value_checkbox);
    case Role.ROLE_IMAGE:
      return context.getString(R.string.value_image);
     case Role.ROLE_IMAGE_BUTTON:
      return context.getString(R.string.value_button) 

Если строка не подошла ни под один из известных типов, роль не будет определена и тип не будет озвучен.

return ROLE_NONE; 

Когда такое может произойти? Можно легко представить ситуацию, когда вместо класса Button мы по каким-то причинам взяли TextView и сделали его кнопкой. 

<TextView						
  android:layout_width="match_parent" 
  android:layout_height="48dp"
  android:layout_gravity="center_vertical" 
  android:layout_marginHorizontal="20dp"   
  android:background="@drawable/round_rect_shape" 
  android:gravity="center"						
  android:text="Оформить заказ" 
  android:textSize="18sp" /> 

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

Также, возможно, есть контейнер, в который мы накидали заголовок, подзаголовок, картинку — это тоже должно восприниматься как кнопка, но при этом тип тоже не будет озвучен, потому, что TalkBack не сможет сматчить ConstraintLayout (или любую другую ViewGroup) ни с чем другим. 

<androidx.constraintlayout.widget.ConstraintLayout						
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content”>
					
<TextView
  android:id="@+id/title"
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content"     
  android:text="@string/invite_friend_title"/>	
				
<TextView
  android:id="@+id/subtitle" 
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" 
  android:text="@string/invite_friend_subtitle"/>
						
<ImageView				
  android:id="@+id/image" 
  android:layout_width="60dp"
  android:layout_height="60dp" 
  android:src="@drawable/discount" />					
</androidx.constraintlayout.widget.ConstraintLayout> 

Поэтому можно переопределить метод getAccessibilityClassName и просто вернуть нужное название класса.

class PromoButton(context: Context): ConstraintLayout(context) { 
  //...
  override fun getAccessibilityClassName(): CharSequence {
    return Button::class.java.name 
  }
}

Проблема может также возникнуть, когда вы создаете custom view, при этом он ведет себя по-другому.

class AdBanner(context: Context): View(context) {
						
  override fun onDraw(canvas: Canvas?) { 
    super.onDraw(canvas)
      //
    }
						
  override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {      
    super.onInitializeAccessibilityNodeInfo(info)
    val node = AccessibilityNodeInfoCompat.wrap(info) node.roleDescription = "Banner"
  } 
} 

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

Состояние 

Например, у переключателя состояние берется из поля mChecked, которое есть в классе switch.

item.setChecked(mChecked); 

Там уже есть реализация, когда значение этого поля просто передается через Node в TalkBack. Если же мы не используем, например, какие-то атрибуты View, а просто для кнопки задаем Alpha и убираем OnClickListener, чтобы она стала неактивной, и она будет выглядеть как неактивная, мы можем тут потерять состояние. 

doneButton.setAlpha(0.5f) 
doneButton.setOnClickListener(null) 

Поэтому тут нужно честно менять его через состояние isEnabled, тогда TalkBack получит актуальное состояние и сможет его озвучить. 

accept.isEnabled = isChecked 

Когда нет типа и состояния, то пользователь может просто зайти в тупик.

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

Подсказка

Подсказка очень полезна для пользователей TalkBack, которые недавно начали его использовать. 

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

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {   
  super.onInitializeAccessibilityNodeInfo(info)   
  info.addAction(AccessibilityNodeInfo.AccessibilityAction(
    AccessibilityNodeInfo.ACTION_CLICK,
      "Применить изменения"
						
    )) 
} 

Не нужно закладывать в подсказку важную информацию: опытные пользователи их отключают, чтобы каждый раз не слушать «Нажмите дважды, чтобы активировать».

А что можно скрывать от пользователей?

  • Разделители и другие декоративные элементы, которые есть на экране, но по сути бесполезные для пользователя. 

  • Пустые элементы — те, которые не видны зрячему пользователю или неактивны в данный момент. 

Для избавления от элементов можно использовать атрибут importantForAccessibility, у которого нужно выставить значение «no»:

<View
  android:id="@+id/divider" 
  android:layout_width="match_parent" 
  android:layout_height="1dp" 
  android:layout_marginHorizontal="8dp" 
  android:importantForAccessibility="no" 
  />
						
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 

Что делать с изображениями? 

Изображение может давать пользователю больше контекста, как в примере на скриншоте. 

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

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

Поэтому можно подписать это как «Увеличенное изображение», чтобы пользователю стало понятнее. 

Тут хотелось бы привести в пример приложение от IKEA. Они сильно озаботились этим вопросом и сделали очень подробное описание того, что находится на изображении.

Похоже, что это не автоматическое распознавание изображений, описания составлялись людьми. Можно подумать на эту тему, возможно, позволить менеджерам или пользователям задавать дополнительное описание элементов или картинок в админке и как-то это потом выводить в приложении. 

Также важно не принимать решения за пользователя, чем он будет пользоваться, а чем нет. Могу привести пример из приложения Яндекс Go. Однажды мы поняли, что Еда и Лавка не очень адаптированы. Наверное, пользователь зашел, чтобы заказать такси, поэтому эти сервисы можно скрыть от него. Тут нас вовремя остановили: у нас есть эксперт по доступности в штате, и он сказал, что так поступать нельзя. 

Анатолий, можешь прокомментировать, почему так делать ни в коем случае не нужно?

Анатолий: Когда мы занимаемся повышением доступности мобильного или любого другого интерфейса, у нас есть принципы или даже целые заповеди. Первая заповедь повышения доступности звучит следующим образом: визуальный и невизуальный интерфейсы должны обеспечивать идентичные функциональность и информативность. Они должны быть настолько одинаковыми, насколько это в принципе возможно. 

Почему так? Если вдруг мы решаем скрыть что-то от незрячего пользователя, то надо понимать, что именно. Принять это решение очень сложно, и мы будем его принимать исходя из наших с вами представлений о мифическом незрячем пользователе. Я не могу с уверенностью сказать, что обладаю такой экспертностью. Я не знаю, как будет вести себя тот или иной незрячий пользователь, какие он изыщет механизмы для того, чтобы обойти те барьеры доступности, которые могут ему встретиться на пути. Поэтому скрывая какие-то функции, мы, условно говоря, не лечим сломавшуюся руку, а вообще ее отрубаем. Это в корне неправильно. 

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

Навигация и порядок обхода

Виктор: В идеале порядок обхода должен проходить слева направо и сверху вниз.

Но это не всегда возможно. Если у нас есть плоский интерфейс, где нет вложенности различных ViewGroup, возможно, порядок обхода изменится. И может даже нарушаться восприятие:

Этот результат будет в таком случае:

<androidx.constraintlayout.widget.ConstraintLayout
  android:layout_width="match_parent" 
  android:layout_height="wrap_content">
						
<TextView
  android:id="@+id/date_title" 
  android:text="Дата доставки" 
  app:layout_constraintLeft_toLeftOf="parent" 
  app:layout_constraintTop_toTopOf="parent" />
						
<TextView
  android:id="@+id/date_value” 
  android:text="28 апреля"
  app:layout_constraintBottom_toBottomOf="parent"   
  app:layout_constraintLeft_toLeftOf="parent"     
  app:layout_constraintTop_toBottomOf="@id/date_title" />
						
<TextView						
  android:id="@+id/address_title" 
  android:text="Адрес доставки" 
  app:layout_constraintEnd_toEndOf="parent"   
  app:layout_constraintRight_toRightOf="parent" 
  app:layout_constraintTop_toTopOf="parent" />
						
<TextView
  android:id="@+id/address_value"
  android:text="Матросова д. 50 кв 78" 
  app:layout_constraintBottom_toBottomOf="parent"    
  app:layout_constraintRight_toRightOf="parent" 
  app:layout_constraintTop_toBottomOf="@id/address_title" />						
</androidx.constraintlayout.widget.ConstraintLayout> 

А чтобы избежать этого, можно использовать атрибуты accessibilityTraversalAfter и accessibilityTraversalBefore (доступны в API 22 и выше), указывающие, перед каким элементом или после какого должен озвучиваться текущий: 
					
<TextView						
  android:id="@+id/date_value”
  android:text="28 апреля" 
  android:accessibilityTraversalAfter="@id/date_title"/> 

Виды навигации

Доступны особые виды навигации, которые позволяют делать это удобнее или быстрее в тех или иных случаях. Навигация по тексту позволяет настроить, как будет озвучен текст:

  • по символам;

  • по словам;

  • по строкам;

  • по абзацам;

  • по заголовкам;

  • по ссылкам;

  • по элементам управления.

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

Чтобы пометить View как заголовок, нужно использовать атрибут accessibilityHeading, в котором мы выставляем значение true. Атрибут доступен в API 28 и выше.

android:accessibilityHeading="true"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {				
  title.isAccessibilityHeading = true 
} 

Объединение элементов

Иногда есть несколько элементов, которые описывают одну информацию. Но для TalkBack они разбиты на несколько маленьких. 

Чем это плохо: ухудшается целостное восприятие, и фокус переключается слишком часто.

Чтобы это поправить, можно на контейнере над этими элементами выставить атрибут focusableInTouchMode в значении true, а эти элементы внутри сделать нефокусируемыми.

<androidx.constraintlayout.widget.ConstraintLayout						
  android:id="@+id/designer_item”
  ... 
  android:focusableInTouchMode="true">						
<TextView
  ...
  android: focusableInTouchMode ="false"/>					
<TextView						
  android:id="@+id/designer"
  ... 
  android:focusableInTouchMode="false"/>						
</androidx.constraintlayout.widget.ConstraintLayout> 

Так фокус один раз выставится на общем контейнере, будут озвучены все вложенные элементы. Вот примеры таких группировок:

Динамическое обновление данных 

Часто возникает ситуация, что после действия пользователя в интерфейсе возникает определенная информация для него, при этом меняется и часть интерфейса, с которой он не взаимодействовал. 

К примеру, тут после тапа на кнопку местоположения меняется адрес. 

Фокус TalkBack по прежнему остается на кнопке «Мое местоположение», но при этом значение текста в шапке изменилось. Этот момент важно озвучить для пользователя. Для этого нужно использовать атрибут AccessibilityLiveRegion. 

ViewCompat.setAccessibilityLiveRegion(this, liveRegion); 

Он есть в трех значениях: 

  • ACCESSIBILITY_LIVE_REGION_NONE (по умолчанию);

  • ACCESSIBILITY_LIVE_REGION_POLITE;

  • ACCESSIBILITY_LIVE_REGION_ASSERTIVE (прерывающий). 

Последний примечателен тем, что он прервет любой озвучиваемый текст в данное время и озвучит изменение, которое произошло. Этот атрибут не всегда работает как нужно, потому что он зачитывает любое изменение, и иногда удобнее использовать метод класса View announceForAccessibility, в котором мы передаем текст, который нужно озвучить. 

Модальные окна AlertDialog, BottomSheetDialog

Отдельной болью была адаптация модальных окон. Обычные AlertDialog и BottomSheetDialog работают следующим образом: когда мы их показываем, фокус попадает внутрь диалога, при перемещении зацикливается внутри него и никуда не выходит. 

AlertDialog.Builder(this) 
  .setTitle("AlertDialog") 
  .setPositiveButton("OK") { _, _ -> } 
  .show() 

Это все заложено в реализацию диалогов. Когда мы начинаем делать кастомные диалоги, допустим, нам понадобилось сделать очередь диалогов или показывать их особым образом, мы можем наследоваться от обычного контейнера; при этом возникает проблема, что фокус из диалога может просто уходить на ту область экрана, которая сейчас неактивна. 

Пример данной проблемы:

Фокус уходит вниз, хотя для зрячего пользователя интерфейс внизу будет недоступен, и он не сможет с ним взаимодействовать. 

В iOS есть атрибут, который решает эту проблему, — accessibilityViewIsModal. Если данный атрибут использовать на каком-то элементе, то фокус не будет уходить из него. 

Нам понадобилось написать свое решение, которое помогло избавиться от этой проблемы. К примеру, у нас есть root-контейнер, который содержит весь UI, и UI некоторого экрана, который закрывается модальным окном. 

После того, как появляется модальное окно, весь UI, который находится под ним, мы делаем неактивным.

Для этого можно использовать атрибут importantForAccessibility со значением noHideDescendants. Это позволяет скрыть от TalkBack не только элемент, на котором мы применили это значение, но и всех его потомков. Мы неким образом запоминаем значения атрибутов у View-элементов, которые мы скрыли. После того, как мы всё показали, и нужно все скрыть, мы достаем старые значения и применяем их на UI. Это достаточно спорное решение, оно может привести к проблемам, но, к сожалению, другого решения не нашлось, сколько бы я ни искал его. 

Custom View

Теперь посмотрим простой пример с переключателем, если вдруг мы написали его, не используя стандартный Switch из Android SDK.

abstract class Switcher @JvmOverloads constructor( 
  ...
) : View(context, attrs, defStyleAttr) {
						
      open var isChecked = true 
        protected set 

Тут у нас есть некий атрибут isChecked, который показывает, включен или выключен переключатель. Если мы не подумали о доступности этого элемента, он будет озвучен как «Без ярлыка». 

Если мы переопределим метод getAccessibilityClassName и подставим туда нужный тип, то наш Switch будет озвучен как «Переключатель». 

abstract class Switcher @JvmOverloads constructor( 
  ...
) : View(context, attrs, defStyleAttr) {
						
      open var isChecked = true 
        protected set 
      override fun getAccessibilityClassName(): CharSequence {
        return Switch::class.java.name 
      } 
}

Но этого недостаточно, потому, что состояние у этого элемента не меняется. Поэтому нам нужно переопределить еще метод onInitializeAccessibilityNodeInfo. Сюда приходит объект AccessibilityNodeInfo, и его нужно заполнять различными значениями, то есть вкладывать туда информацию об этом элементе, которая будет потом прочитана TalkBack. 

open var isChecked = true 
  protected set
override fun getAccessibilityClassName(): CharSequence {
  return Switch::class.java.name 
}
						
override fun onInitializeAccessibilityNodeInfo(
  info: AccessibilityNodeInfo 
){
  super.onInitializeAccessibilityNodeInfo(info)
  info.isChecked = isChecked 
} 

Тут мы указываем значение isChecked, и элемент для TalkBack уже зачитывается как «Отмечено» или «Не отмечено». 

Тут добавляется еще один атрибут isCheckable, который говорит, что этот элемент может менять свое состояние (строка info.isCheckable = true).

open var isChecked = true 
  protected set
override fun getAccessibilityClassName(): CharSequence {
  return Switch::class.java.name 
}
						
override fun onInitializeAccessibilityNodeInfo(
  info: AccessibilityNodeInfo 
){
					
  super.onInitializeAccessibilityNodeInfo(info) 
  info.isCheckable = true
  info.isChecked = isChecked
} 

При взаимодействии с элементом новое состояние озвучивается моментально. Возможно, у вас есть сложный элемент, представленный как один View (например, шкала оценки). То есть что-то нарисовано на Сanvas, всё отрисовывается вручную. При этом View-элемент предполагает, что у него есть составные элементы, которые требуют отдельного фокуса и с которыми можно отдельно взаимодействовать.

Не буду углубляться в детали, но решение для этого тоже есть. Нужно использовать класс ExploreTouchHelper и переопределить несколько методов. 

protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds); 
protected abstract int getVirtualViewAt(float x, float y);						
protected abstract void onPopulateNodeForVirtualView(
  int virtualViewId, @NonNull AccessibilityNodeInfoCompat node);						
protected abstract boolean onPerformActionForVirtualView( 
  int virtualViewId, int action, @Nullable Bundle arguments); 

Нужно сказать, сколько частей есть у этого View-элемента, передать область экрана, в которой находятся эти элементы, и заполнить их метаинформацией, которая будет использована TalkBack.

Сканер доступности

У Google были попытки автоматизировать проверки, связанные с доступностью. Они написали приложение «Сканер доступности», которое позволяет запустить приложение и получить отчет о проблемах с доступностью, которые в нём есть. Приложение устанавливается из магазина и запускается через меню доступности. 

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

Количество проверок ограничено. Есть проверка области прикосновения: она не должна быть очень маленькой — там есть минимальный порог в 48 dp. Есть проверка на проблемы, связанные с перекрытием кликабельных элементов и контрастностью текста. Приложение дает рекомендации по тексту относительно фона, например, какой цвет текста лучше подойдет. 

Опыт Яндекс Go

Пара слов о нашем опыте. У нас была пара попыток адаптации доступности. Первая попытка была еще в 2018 году, у нас тогда зарождалась дизайн-система, и мы сделали ее элементы доступными. 

Это помогло, потому что после того, как мы добавили много новых экранов и функций, но использовали компоненты из дизайн-системы, даже разработчики, которые были не знакомы с проблемами доступности, делали уже доступный интерфейс. Элементы комбинировались: если у них были заголовки, подзаголовки, то они зачитывались как один элемент. Также мы поместили туда типы кнопок, переключателей — всё это ради доступности.

К сожалению, с появлением новых элементов мы забыли о доступности. Пришлось делать второй подход к решению проблемы. Тут мы подошли более основательно и поняли, что нужно регулярно тестировать доступность, так как она имеет свойство ломаться с релизами. 

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

Анатолий, хотелось бы услышать твой комментарий, как подойти к вопросу доступности, если у нас есть большое приложение, и мы знаем, что там есть проблема. 

Анатолий: Да, мне часто задают вопрос: «С чего начать адаптацию приложения?» У меня есть несколько советов. 

Во-первых, я бы, конечно, пригласил к диалогу пользователей. Используйте каналы коммуникации с пользователями, можно использовать инструмент Release notes. Напишите: «Мы думаем о повышении доступности программы экранного доступа, и если вам есть, что сказать, пишите». 

Параллельно можно посмотреть ролики, как используются приложения без визуального контроля. В YouTube довольно много таких роликов. 

Позовите эксперта-пользователя вашего приложения и организуйте его встречу с командой. Это мотивирующий шаг: вся команда уже не будет абстрактно думать, что есть мифические незрячие пользователи. Когда вся команда собирается и смотрит, перенимает опыт использования приложения без визуального контроля, понимает, что это всё не шутки и есть живые пользователи, видит, как они взаимодействуют с вашим приложением, — это сразу придает работе смысл и очень сильно мотивирует. И вы сразу увидите слабые места, требующие починки, ведь понятно, что что не будешь весь код читать и править.

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

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

А дальше вы уже можете начать читать книги о том, как делать доступными приложения.

Виктор: Добавлю, что это постепенный процесс, и за неделю, это, наверное, не сделать. 

Анатолий: Я с тобой соглашусь и скажу, что иногда проблема доступности вызывает сильный стресс у разработчиков. Хочется прямо сейчас побежать и всё сделать. Нет, не нужно, просто поставьте это в приоритет, позанимайтесь доступностью по полчаса в день, например, в течение месяца. Когда это станет привычной рутинной работой, когда будет меньше вопросов о том, как и зачем — тогда это всё войдет в колею. Надрываться не надо — вот какая у меня мысль еще, немного крамольная для этого доклада.

Виктор: Надеюсь, я смог доказать, что это не сложно. Думаю, если мы чуть-чуть повысим свою экспертность и просто будем уже в процессе разработки задумываться сразу о проблемах доступности, то мы много чего сможем исправить и прийти к светлому будущему. 

Следующий Mobius пройдёт 23-26 мая. Программа пока что формируется, так что ещё не сказать, будут ли доклады конкретно про accessibility. Но, как и в этом докладе, точно будет техническая конкретика для мобильных разработчиков — если вы в их числе, обратите внимание.

Комментарии (0)