Реализация доступности клавиатуры в Flutter
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Как сделать ваше приложение более инклюзивным
Вы когда-нибудь пробовали пользоваться смартфоном без прикосновений? Представьте, что вы пытаетесь использовать приложение, но вместо нажатий или свайпов вам нужно управлять им полностью с помощью внешней клавиатуры. Это может показаться необычным для многих вначале, но для других это может быть необходимо.
Доступность — важная часть разработки современных приложений не только из-за юридических требований, но и для улучшения пользовательского опыта для всех пользователей. Навигация с клавиатурой — фундаментальный аспект доступности приложений, так как она позволяет пользователям взаимодействовать с приложениями без использования жестов. Это особенно важно для людей с нарушениями двигательных функций, которые могут испытывать трудности с использованием сенсорных экранов, а также для пользователей, которые предпочитают внешние клавиатуры для повышения эффективности или удобства. Предоставление полной доступности клавиатуры гарантирует, что все интерактивные элементы могут быть достигнуты и активированы, создавая более инклюзивный и дружелюбный пользовательский опыт.
В этой статье вы узнаете, как сделать ваше приложение Flutter управляемым с клавиатуры, чтобы оно было полностью доступно для тех, кто на это полагается. В ней обсуждаются классы и виджеты Flutter, которые помогают вам этого достичь, и что вам нужно учитывать. Все, что вам нужно для этого — клавиатура для управления приложением (или эмулятор с поддержкой клавиатуры).
Реализация навигации с клавиатуры
Управление фокусом и навигацией
Пользователи клавиатуры обычно используют клавишу Tab (и Shift+Tab) для навигации по интерактивным элементам приложения. Каждый интерактивный элемент должен быть доступен с помощью клавиш. Некоторые виджеты, такие как TextField, автоматически получают фокус, в то время как другие интерактивные элементы, такие как кнопки, элементы списков или пользовательские виджеты, требуют FocusNode для доступности. FocusNode — это объект, который может быть использован состоятельным виджетом для управления его состоянием фокуса. Он указывает, имеет ли виджет в данный момент фокус или нет. Вы можете установить или снять фокус с определенного элемента, реагировать на изменения фокуса и обрабатывать события клавиш.
Focus — это виджет, который оборачивает дочерний виджет и управляет его состоянием фокуса. Он автоматически создает FocusNode, или вы можете установить пользовательский FocusNode для наблюдения за фокусом или управления поведением виджета при получении или потере фокуса.
final focusNode = FocusNode();
Focus(
focusNode: focusNode
child: CustomButton(
onPressed: () {
print('Button 1 pressed');
},
),
);
Порядок навигации
Порядок, в котором интерактивные элементы должны получать фокус, очень важен. Порядок навигации должен быть логичным и интуитивным и должен следовать визуальному потоку. Для большинства страниц это означает паттерн навигации сверху вниз и слева направо. Используйте FocusTraversalGroup для группировки связанных виджетов, чтобы они проходили в определенном порядке. Он определяет только порядок внутри группы, но не ограничивает движение за ее пределами. Это означает, что после того, как все элементы в группе получили фокус, фокус перейдет к следующему виджету за пределами группы. OrderedTraversalPolicy определяет, как элементы внутри группы проходят. Он следует порядку, определенному в дереве виджетов, если не переопределен с помощью FocusTraversalOrder. Если требуется пользовательский порядок навигации, используйте FocusTraversalOrder с NumericFocusOrder для явного определения последовательности фокуса.

FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
Row(
children: [
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextButton(
onPressed: () {},
child: const Text('Button 1'),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextButton(
onPressed: () {},
child: const Text('Button 2'),
),
),
],
),
Row(
children: [
FocusTraversalOrder(
order: const NumericFocusOrder(4),
child: TextButton(
onPressed: () {},
child: const Text('Button 4'),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: TextButton(
onPressed: () {},
child: const Text('Button 3'),
),
),
],
),
],
),
);
Если требуется более строгая граница фокуса, вы можете использовать FocusScope. Это ограничивает определенные области для изменения фокуса, что означает, что фокус останется внутри области, если он не будет явно перемещен. Но очень важно избегать ловушек клавиатуры, когда пользователи застревают внутри определенного элемента и не могут выйти из него.

Column(
children: [
FocusScope(
child: Column(
children: [
TextButton(
onPressed: () {},
child: const Text('Button 1'),
),
TextButton(
onPressed: () {},
child: const Text('Button 2'),
),
],
),
),
TextButton(
onPressed: () {},
child: const Text('Button 3'),
),
],
);
Управление прокручиваемым контентом
Прокручиваемый контент, такой как списки или длинные тексты, также должен быть доступен для навигации с помощью клавиатуры. Это можно достичь, например, с помощью клавиш со стрелками. Класс Flutter HardwareKeyboard слушает ввод с физической клавиатуры. Чтобы оставаться в курсе, когда клавиши нажимаются, удерживаются или отпускаются, вы можете добавить обработчик и отреагировать на событие.
Приведённый ниже пример демонстрирует, как можно реализовать прокрутку с помощью клавиатуры. Объект ScrollController управляет прокруткой, а слушатель HardwareKeyboard обнаруживает нажатия клавиш со стрелками. Это позволяет пользователям перемещаться по прокручиваемому содержимому с помощью клавиш со стрелками. Проверка scrollController.position.outOfRange перед выполнением анимации прокрутки делает её более естественной, предотвращая резкую остановку прокрутки при достижении начала или конца содержимого.
HardwareKeyboard.instance.addHandler((keyEvent) {
if (keyEvent is! KeyDownEvent || scrollController.position.outOfRange) {
return false;
}
final offset = scrollController.offset;
const scrollDuration = Duration(milliseconds: 300);
const offsetChange = 50;
if (keyEvent.logicalKey == LogicalKeyboardKey.arrowDown) {
scrollController.animateTo(
offset + offsetChange,
duration: scrollDuration,
curve: Curves.linear,
);
} else if (keyEvent.logicalKey == LogicalKeyboardKey.arrowUp) {
scrollController.animateTo(
offset - offsetChange,
duration: scrollDuration,
curve: Curves.linear,
);
}
return false;
});Во время прокрутки может возникнуть ситуация, когда вы хотите продолжить навигацию с помощью клавиш Tab или Shift + Tab, но следующий фокусируемый виджет не виден. Необходимо гарантировать, что текущий фокусируемый виджет всегда виден. Для этой цели можно реализовать слушатель на FocusManager. FocusManager — это синглтон, который отвечает за отслеживание того, какой FocusNode имеет основной фокус, порядок узлов и распределение событий клавиатуры узлам в дереве фокуса.
FocusManager.instance.addListener(() {
final focusedNodeContext = FocusManager.instance.primaryFocus?.context;
if (focusedNodeContext == null || !context.mounted) {
return;
}
Scrollable.ensureVisible(
focusedNodeContext,
curve: Curves.linear,
duration: _scrollDuration,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
);
}); Пользовательские сочетания клавиш
Для виджетов, к которым нельзя добраться с помощью Tab или Shift + Tab, можно определить пользовательские сочетания клавиш. Это можно сделать, добавив слушатели для определённых клавиш или используя виджет Shortcuts из Flutter. Обратите внимание, что сочетания клавиш работают только тогда, когда виджет в данный момент имеет фокус.
Виджет Shortcuts связывает LogicalKeySet (комбинацию одной или нескольких клавиш) с Intent. Затем виджет Actions связывает Intent с Action. Когда нажимается комбинация клавиш, виджет Shortcuts запускает соответствующий Intent, который виджет Actions преобразует в действие, вызывая его метод invoke().
Приведённый ниже пример показывает, как можно использовать сочетания клавиш в текстовом редакторе с пакетом Quill. Пользователь может форматировать текст (например, делать его жирным), но простое использование клавиши Tab для перемещения фокуса на кнопки не подходит, так как это просто отступит текст, а не переместит фокус. Поэтому необходимо добавить сочетания клавиш (CTRL + B) для выполнения действий по форматированию без влияния на ввод текста. Кроме того, реализован слушатель для клавиши escape, чтобы выйти из редактора и переместить фокус на следующий виджет.

class CustomQuillEditor extends StatelessWidget {
final QuillController controller;
final FocusNode focusNode;
final FocusNode nextFocusNode;
const CustomQuillEditor({
required this.controller,
required this.focusNode,
required this.nextFocusNode,
super.key,
});
@override
Widget build(BuildContext context) {
HardwareKeyboard.instance.addHandler((keyEvent) {
if (keyEvent is! KeyDownEvent) {
return false;
}
if (focusNode.hasFocus && keyEvent.logicalKey == LogicalKeyboardKey.escape) {
FocusScope.of(context).unfocus();
nextFocusNode.requestFocus();
}
return false;
});
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyB):
const _TextEditingIntent(Attribute.bold),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyI):
const _TextEditingIntent(Attribute.italic),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyU):
const _TextEditingIntent(Attribute.underline),
},
child: Actions(
actions: <Type, Action<Intent>>{
_TextEditingIntent: _TextEditingAction(controller),
},
child: QuillEditor(
focusNode: focusNode,
controller: controller,
scrollController: ScrollController(),
),
),
);
}
}
class _TextEditingIntent extends Intent {
final Attribute attribute;
const _TextEditingIntent(this.attribute);
}
class _TextEditingAction extends Action<_TextEditingIntent> {
_TextEditingAction(this.controller);
final QuillController controller;
@override
void invoke(covariant _TextEditingIntent intent) {
controller
..skipRequestKeyboard = !intent.attribute.isInline
..formatSelection(intent.attribute);
}
}Заключение
Навигация с помощью клавиатуры — важный аспект доступности мобильных приложений, играющий ключевую роль в создании инклюзивных пользовательских интерфейсов. Обеспечение того, что все интерактивные элементы могут быть фокусируемы, позволяет пользователям с нарушениями подвижности или использующим внешние клавиатуры взаимодействовать с приложением без барьеров. Поддержание логического порядка фокусировки способствует интуитивному потоку навигации, а управление прокручиваемым содержимым гарантирует доступность даже не нажимаемых элементов. Пользовательские сочетания клавиш повышают удобство использования, позволяя быстро получать доступ к частым действиям. Flutter предоставляет мощные инструменты, такие как Focus, FocusTraversalGroup, Shortcuts, и Actions, чтобы помочь разработчикам связывать намерения пользователя с конкретными действиями, обеспечивая плавный и доступный опыт навигации с клавиатурой.
Реализуя эти функции, разработчики могут значительно улучшить удобство использования и доступность приложений для разнообразной аудитории. Давайте создадим воздействие вместе и сделаем наши приложения более доступными для всех! Вы когда-нибудь работали с доступностью клавиатуры в Flutter? Какие трудности вы испытывали? Поделитесь своими мыслями и давайте обсудим! (Автор: Tabea Schuler)