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

Во Flutter есть большое количество виджетов по умолчанию, которые генерируют дерево доступности автоматически. Дерево доступности представляет собой дерево объектов доступности, к которым вспомогательные технологии могут обращаться за атрибутами и свойствами и выполнять действия. Для настраиваемых виджетов класс Semantics позволяет разработчикам описывать значение своих виджетов, помогая вспомогательным технологиям понять смысл содержимого виджетов.

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

import 'package:flutter/semantics.dart';

void main() {
  runApp(const MyApp());
  if (kIsWeb) {
    SemanticsBinding.instance.ensureSemantics();
  }
}

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

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

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

Включение поддержки доступности в Flutter

Механизм явного включения в Flutter реализуется с помощью скрытой кнопки. Он помещает кнопку, а точнее, элемент <flt-semantics-placeholder> с role="button" — невидимый и недоступный для зрячих пользователей — в HTML. Это настраиваемый элемент с наложенным стилем, поэтому он не отображается и не выбирается, если вы не используете скринридер.

<flt-semantics-placeholder
  role="button"
  aria-live="polite"
  aria-label="Enable accessibility"
  tabindex="0"
  style="  
        position: absolute;  
        left: -1px;  
        top: -1px;  
        width: 1px;  
        height: 1px;"
></flt-semantics-placeholder>
/* `<flt-semantics-placeholder>` inherits from `<flutter-view>`. */
flutter-view {
  user-select: none;
}

Изменения после включения

Что происходит, когда пользователь скринридера нажимает на эту кнопку? Рассмотрим на примере карточки из галереи Flutter, как показано на следующем скриншоте.

A classic card component with an image, a heading, and some text.
Классический компонент карточки с изображением, заголовком и текстом.

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

До явного включения:

Chrome DevTools showing an “Enable accessibility” button.
В Chrome DevTools появилась кнопка «Включить доступность».

После явного включения:

Chrome DevTools showing a rich accessibility tree with headings, buttons, groups, etc.
Chrome DevTools показывает богатое дерево доступности с заголовками, кнопками, группами и т. д.

Детали реализации

Основная идея Flutter заключается в создании доступной структуры DOM, которая отражает то, что в данный момент отображается на холсте. Это включает в себя родительский элемент <flt-semantics-host>, который содержит дочерние элементы <flt-semantics> и <flt-semantics-container>, которые, в свою очередь, могут быть вложены друг в друга. Рассмотрим виджет кнопки, например TextButton. Этот виджет представлен элементом <flt-semantics> в DOM. Аннотации ARIA (например, role или aria-label) и другие свойства DOM (tabindex, слушатели событий) на элементе <flt-semantics> позволяют скринридеру объявлять пользователю элемент как кнопку, а также поддерживать щелчки и тапы по нему, даже если это не буквально элемент <button>. На следующем скриншоте кнопка «Поделиться» является примером такой кнопки.

Chrome DevTools showing an absolutely positioned `flt-semantics` element with role `button` for the Share button.
Chrome DevTools показывает абсолютно позиционируемый элемент flt-semantics с ролью button для кнопки «Поделиться».

Этот элемент <flt-semantics> позиционируется абсолютно, чтобы появляться точно в том месте, где на холсте нарисована соответствующая кнопка. Это происходит потому, что Flutter контролирует размещение всех виджетов и заранее рассчитывает позиции и размеры каждого семантического узла. Абсолютный layout позволяет разместить элемент доступности именно там, где его ожидает пользователь. Однако это также означает, что каждый раз, когда пользователь скроллит страницу, позиции необходимо корректировать, что в некоторых ситуациях может быть затратно.

Chrome DevTools показывает, как абсолютно позиционируемые элементы переставляются в реальном времени, когда пользователь прокручивает страницу. Оригинал gif-ки см. в оригинале на medium
Chrome DevTools показывает, как абсолютно позиционируемые элементы переставляются в реальном времени, когда пользователь прокручивает страницу. Оригинал gif-ки см. в оригинале на medium

Применяем подход на все виджеты по умолчанию

Поскольку Flutter знает, что то, что представлено как <flt-semantics role="button"> в структуре DOM в исходном коде Flutter, изначально было Flutter TextButton, становится несложно расширить подход и создать отображение всех существующих Flutter виджетов на соответствующие роли WAI-ARIA, что Flutter и делает из коробки для всех своих виджетов по умолчанию. Например, на данный момент Flutter поддерживает следующие роли:

  • Текст

  • Кнопки

  • Чекбоксы

  • Радиокнопки

  • Текстовые поля

  • Ссылки

  • Диалоги

  • Изображения

  • Слайдеры

  • «Живые» области

  • Прокручиваемые элементы

  • Контейнеры и группы

Обратите внимание: несмотря на короткий список ролей, многие различные категории виджетов часто имеют одинаковую роль. Например, Material TextField и CupertinoTextField могут разделять роль текстового поля. Большинство layout виджетов, таких как Stack, Column, Row, Flex и другие, могут быть представлены контейнером или группой.

Трудности с пользоватескими виджетами 

При создании пользовательского виджета Flutter может не назначить ему корректную роль автоматически. Если виджет просто является декорированным вариантом существующего виджета (например, оболочка над EditableText), он может корректно отображаться (как текстовое поле). Однако, если вы создаете виджет с нуля, Flutter ожидает, что вы используете виджет Semantics для описания его свойств доступности. WAI-ARIA определяет различные роли для виджетов. Flutter поддерживает только подмножество этих ролей, хотя этот список постоянно расширяется.

Например, можно исследовать выбор класса команды в игре I/O Flip в режиме реального времени, как показано на следующем скриншоте. В терминах веба, это по сути <select>, или listbox в терминах WAI-ARIA. Несмотря на то, что доступные варианты представлены как generic text (они должны быть элементами <option>), еще большей проблемой является то, что из дерева доступности не ясно, что существуют варианты для выбора, которые находятся за пределами области просмотра виджета. Обратите внимание на доступные варианты в дереве доступности до и после прокрутки.

До прокрутки:

Chrome DevTools showing the selectable items of a name selector widget in the accessibility tree. It’s cut, though, and doesn’t show all options that are visible after scrolling.
Chrome DevTools показывает выбираемые элементы виджета в дереве доступности. Правда, он урезан и не показывает все варианты, которые видны после прокрутки.

После прокрутки:

Chrome DevTools showing the selectable items of a name selector widget in the accessibility tree, this time after scrolling. It’s cut, though, and doesn’t show all options that were visible before scrolling.
Chrome DevTools показывает выбираемые элементы виджета в дереве доступности, на этот раз после прокрутки. Правда, он урезан и не показывает все варианты, которые были видны до прокрутки.

Если вы посмотрите на исходный код, то увидите, что он не использует класс Semantics, поскольку Semantics пока не поддерживает аннотацию listbox и option role. Но он использует ListWheelScrollView, который похож на обычный ListView — поэтому он знает, что имеет дело со списком. Обратите внимание, что дерево доступности всегда показывает только видимые элементы, а также несколько элементов выше и ниже области просмотра, но никогда не все элементы. (Это обычный трюк для повышения производительности приложений, который мы почти реализовали в виде <virtual-scroller>).

Chrome DevTools showing how the selectable items in a listview get refreshed in realtime in the accessibility tree when the user scrolls.
Chrome DevTools показывает, как выбираемые элементы в listview обновляются в реальном времени в дереве доступности при прокрутке пользователем.

Сравните дерево доступности Flutter с примером прокручиваемого списка из ARIA Authoring Practices Guide, где в дереве доступности отображаются все варианты — даже те, которые находятся за пределами области просмотра. На момент написания этой статьи, неполная поддержка этого случая использования списка (listbox) является недостатком решения Flutter, который будет решен в будущем.

Chrome DevTools showing the accessibility tree of a class HTML `select` element where all items are always announced, independent of what’s visible and what position the element is scrolled to.
Chrome DevTools показывает дерево доступности элемента класса HTML select, в котором все элементы всегда объявляются, независимо от того, что видимо и до какой позиции прокручивается элемент.

Редактирование текста 

В Flutter есть элемент <flt-text-editing-host>, который в качестве дочернего элемента содержит либо <input>, либо <textarea>, и он размещается с точностью до пикселя на соответствующей области холста. Это значит, что такие удобства браузера, как автозаполнение, работают как ожидается. Эта функция всегда включена, независимо от того, включена ли доступность или нет. В дереве семантики текстовое поле представлено элементом <input>, который потенциально имеет ARIA-метку для его описания. Следующий пример текстового поля взят из Flutter Gallery. Посмотрите, как поле <input> динамически перемещается каждый раз, когда пользователь нажимает клавишу Tab.

Chrome DevTools showing how an HTMLtext input field is dynamically repositioned pixel-perfectly over the corresponding canvas-rendered text input fields when the user tabs between the input fields.
Инструментарий Chrome DevTools показывает, как поле ввода HTML-текста динамически pixel-perfectly позиционируется над соответствующими полями ввода текста, отрисованными на холсте, когда пользователь переходит от одного поля ввода к другому.

В то время как для зрячих пользователей тексты меток, отображаемые в текстовых вводах, видны, для пользователей скринридеров текстовые поля объявляются как «edit, blank» с NVDA на Windows или «edit text, blank» с VoiceOver на macOS, поскольку Flutter на данный момент еще не создает элементы <label>. Вы можете увидеть вывод экранной читалки VoiceOver в нижней части изображений. Flutter исправит это в будущем.

Screen reader output rendered to the screen showing the screen reader ignores the user-visible labels and announces just a blank text field without context.
При выводе на экран скринридер игнорирует видимые пользователем подписи и выдает просто пустое текстовое поле без контекста.

Когда текстовые поля правильно помечены, скринридер объявляет их предполагаемое значение, как показано в следующем примере на чистом HTML.

Screen reader output rendered to the screen, this time for a classic HTML form, showing the screen reader respect the user-visible labels and announce the text fields with context.
Вывод на экран с помощью скринридера, на этот раз для классической HTML-формы, показывающий, что скринридер учитывает видимые пользователем подписи и объявляет текстовые поля с контекстом.

Заключение 

В этой публикации мы углубились в тонкости того, как работает поддержка доступности в приложениях на Flutter с использованием холста в вебе. Доступность в Flutter реализуется через скрытую кнопку с определёнными атрибутами и стилем. При активации этот подход значительно улучшает опыт пользователей, полагающихся на скринридеры и другие вспомогательные технологии. Основная концепция в Flutter заключается в создании доступной DOM-структуры, которая отражает содержимое холста, используя такие пользовательские элементы, как <flt-semantics-host>, <flt-semantics>, <flt-semantics-container> и другие. Хотя Flutter умело сопоставляет стандартные виджеты с ролями WAI-ARIA, команда признаёт наличие некоторых проблем. Исследование редактирования текста в Flutter демонстрирует приём с <flt-text-editing-host>, содержащим <input> или <textarea>, что показывает динамическое перемещение полей ввода.

Забегая вперёд, отметим, что команда уже начала работать над совершенствованием системы доступности Flutter. В числе задач — решение проблемы использования listbox для пользовательских виджетов и улучшение создания элементов label для редактирования текста. Ожидается, что эти улучшения обеспечат более полный и нерперывный опыт доступности, что отражает стремление Flutter к постоянному совершенствованию своей цели по компиляции веб-страниц.


В заключение приглашаем всех желающих на открытый урок 24 июля, который посвящен тому, как делать Flutter-приложения плавным и отзывчивым.

На занятии мы поговорим о типичных проблемах, из-за которых возникают зависания интерфейса (в том числе на Impeller), научимся их обнаруживать с помощью инструментов DevTools, Perfetto и интегрировать замеры производительности в код с помощью dart:developer. Также мы посмотрим принципы работы ServiceExtensions и создадим собственное расширение для отслеживания виджетов с потенциальными проблемами. Для примера будем оптимизировать несложную игру с большим количеством визуальных эффектов, из-за которых в исходном варианте не получается достичь ожидаемых 60 кадров в секунду.

Записаться на урок можно на странице курса «Flutter Mobile Developer».

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