用户输入与无障碍功能
一个真正自适应的应用程序,不仅要处理用户输入方式的差异,还要提供能够帮助残障人士的辅助功能。
仅仅调整应用程序的外观是不够的,你还必须支持多种用户输入方式。鼠标和键盘引入了触摸设备所没有的输入类型,例如滚轮、右键单击、悬停交互、Tab 键遍历和键盘快捷键。
其中一些功能在 Material 组件中是默认生效的。但是,如果你创建了自定义组件,可能需要直接实现这些功能。
一些优秀应用所具备的功能,同时也能够帮助使用辅助技术的用户。例如,除了作为优秀的应用程序设计之外,像 Tab 键遍历和键盘快捷键这类功能,对于使用辅助设备的用户来说至关重要。除了关于创建无障碍应用的标准建议外,本页面还涵盖了创建既自适应又具备无障碍支持的应用程序的相关信息。
自定义组件的滚轮操作
#像 ScrollView 或 ListView 这样的滚动组件默认支持滚轮,而且由于几乎所有可滚动的自定义组件都是基于这些组件构建的,因此它们也同样支持滚轮操作。
如果你需要实现自定义的滚动行为,可以使用 Listener 组件,它允许你自定义 UI 对滚轮的反应方式。
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) print(event.scrollDelta.dy);
},
child: ListView(),
);
Tab 键遍历与焦点交互
#使用物理键盘的用户期望能够通过 Tab 键快速导航应用程序,而有运动障碍或视觉障碍的用户通常完全依赖键盘导航。
Tab 交互有两个注意事项:焦点如何在组件之间移动(即遍历),以及组件获得焦点时显示的视觉高亮效果。
大多数内置组件(如按钮和文本字段)默认支持遍历和高亮显示。如果你有想要包含在遍历中的自定义组件,可以使用 FocusableActionDetector 组件来创建自己的控件。FocusableActionDetector 组件非常有用,它能将焦点、鼠标输入和快捷键组合到一个组件中。你可以创建一个定义操作和键绑定的检测器,并提供用于处理焦点和悬停高亮的回调。
class _BasicActionDetectorState extends State<BasicActionDetector> {
bool _hasFocus = false;
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
onFocusChange: (value) => setState(() => _hasFocus = value),
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<Intent>(
onInvoke: (intent) {
print('Enter or Space was pressed!');
return null;
},
),
},
child: Stack(
clipBehavior: Clip.none,
children: [
const FlutterLogo(size: 100),
// Position focus in the negative margin for a cool effect
if (_hasFocus)
Positioned(
left: -4,
top: -4,
bottom: -4,
right: -4,
child: _roundedBorder(),
),
],
),
);
}
}
控制遍历顺序
#若要更好地控制用户在进行 Tab 键切换时组件获得焦点的顺序,可以使用 FocusTraversalGroup 来定义树中的某些部分,这些部分在 Tab 切换时应被视为一个组。
例如,你可能希望在 Tab 到提交按钮之前,先遍历表单中的所有字段。
return Column(
children: [
FocusTraversalGroup(child: MyFormWithMultipleColumnsAndRows()),
SubmitButton(),
],
);
Flutter 提供了多种内置方式来遍历组件和组,默认使用 ReadingOrderTraversalPolicy 类。该类通常效果很好,但也可以通过使用其他预定义的 TraversalPolicy 类或创建自定义策略来进行修改。
键盘快捷键
#除了 Tab 键遍历,桌面端和 Web 端用户习惯于将各种键盘快捷键绑定到特定操作。无论是用于快速删除的 Delete 键,还是用于新建文档的 Control+N,请务必考虑用户所期望的不同快捷键。键盘是一个强大的输入工具,因此请尽量从中榨取效率。你的用户会感激你的!
在 Flutter 中,根据你的目标,可以通过几种方式实现键盘快捷键。
如果你有一个像 TextField 或 Button 这样已经拥有焦点节点的单个组件,你可以将其包装在 KeyboardListener 或 Focus 组件中,并监听键盘事件。
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
print(event.logicalKey);
}
return KeyEventResult.ignored;
},
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: const TextField(
decoration: InputDecoration(border: OutlineInputBorder()),
),
),
);
}
}
要将一组键盘快捷键应用于树的大部分区域,请使用 Shortcuts 组件。
// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
const CreateNewItemIntent();
}
Widget build(BuildContext context) {
return Shortcuts(
// Bind intents to key combinations
shortcuts: const <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.keyN, control: true):
CreateNewItemIntent(),
},
child: Actions(
// Bind intents to an actual method in your code
actions: <Type, Action<Intent>>{
CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
onInvoke: (intent) => _createNewItem(),
),
},
// Your sub-tree must be wrapped in a focusNode, so it can take focus.
child: Focus(autofocus: true, child: Container()),
),
);
}
Shortcuts 组件之所以有用,是因为它仅在该组件树或其子组件获得焦点且可见时才允许触发快捷键。
最后一个选项是全局监听器。此监听器可用于始终开启的、全应用范围的快捷键,或用于在可见时(无论焦点状态如何)均可接收快捷键的面板。使用 HardwareKeyboard 可以轻松添加全局监听器。
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(_handleKey);
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKey);
super.dispose();
}
要使用全局监听器检查按键组合,可以使用 HardwareKeyboard.instance.logicalKeysPressed 集合。例如,以下方法可以检查所提供的按键中是否有任何按键被按下。
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
return keys
.intersection(HardwareKeyboard.instance.logicalKeysPressed)
.isNotEmpty;
}
将这两者结合起来,你就可以在按下 Shift+N 时触发一个操作。
bool _handleKey(KeyEvent event) {
bool isShiftDown = isKeyDown({
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
});
if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
_createNewItem();
return true;
}
return false;
}
使用静态监听器时需要注意一点:当用户在字段中输入文字或关联的组件被隐藏时,通常需要禁用它。与 Shortcuts 或 KeyboardListener 不同,管理这一状态是你的责任。当你为 Delete 绑定删除/退格快捷键,但同时又有用户可能正在输入的子 TextField 时,这一点尤为重要。
自定义组件的鼠标进入、离开与悬停
#在桌面端,更改鼠标光标以指示鼠标悬停内容的功能是一种常见做法。例如,当你悬停在按钮上时通常会看到手形光标,或者在悬停于文本上时看到 I 形光标。
Flutter 的 Material 按钮可以处理标准按钮和文本光标的基本焦点状态。(一个显著的例外是,如果你更改了 Material 按钮的默认样式,将 overlayColor 设置为透明。)
请为应用中的任何自定义按钮或手势检测器实现焦点状态。如果你更改了默认的 Material 按钮样式,请测试键盘焦点状态,并在必要时实现自己的焦点状态。
要在自定义组件中更改光标,请使用 MouseRegion。
// Show hand cursor
return MouseRegion(
cursor: SystemMouseCursors.click,
// Request focus when clicked
child: GestureDetector(
onTap: () {
Focus.of(context).requestFocus();
_submit();
},
child: Logo(showBorder: hasFocus),
),
);
MouseRegion 对于创建自定义的悬停(Rollover/Hover)效果也很有用。
return MouseRegion(
onEnter: (_) => setState(() => _isMouseOver = true),
onExit: (_) => setState(() => _isMouseOver = false),
onHover: (e) => print(e.localPosition),
child: Container(
height: 500,
color: _isMouseOver ? Colors.blue : Colors.black,
),
);
关于如何在按钮获得焦点时更改样式以显示轮廓的示例,请查看 Wonderous 应用的按钮代码。该应用通过修改 FocusNode.hasFocus 属性来检查按钮是否获得焦点,如果是,则添加一个轮廓。
视觉密度
#例如,你可能需要考虑扩大组件的“点击区域”以适配触摸屏。
不同的输入设备提供不同级别的精度,这需要不同大小的点击区域。Flutter 的 VisualDensity 类可以轻松调整整个应用程序的视图密度,例如,使按钮在触摸设备上变大(从而更容易点击)。
当你更改 MaterialApp 的 VisualDensity 时,支持此属性的 MaterialComponents 会根据密度进行动画调整。默认情况下,水平和垂直密度都设置为 0.0,但你可以将密度设置为你想要的任何负值或正值。通过在不同的密度之间切换,你可以轻松调整 UI。
要设置自定义视觉密度,请将密度注入你的 MaterialApp 主题中。
double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density = VisualDensity(
horizontal: densityAmt,
vertical: densityAmt,
);
return MaterialApp(
theme: ThemeData(visualDensity: density),
home: MainAppScaffold(),
debugShowCheckedModeBanner: false,
);
要在你自己的视图中使用 VisualDensity,你可以直接查找它。
VisualDensity density = Theme.of(context).visualDensity;
容器不仅会自动对密度变化做出反应,而且在变化时还会进行动画处理。这有效地将你的自定义组件与内置组件结合在一起,使整个应用程序呈现出流畅的过渡效果。
如所示,VisualDensity 是无单位的,因此它可以对不同的视图意味着不同的含义。在以下示例中,1 个密度单位等于 6 个像素,但这完全取决于你自己的决定。无单位的事实使其非常通用,应该适用于大多数上下文。
值得注意的是,Material 组件通常为每个视觉密度单位使用约 4 个逻辑像素的值。有关支持组件的更多信息,请参阅 VisualDensity API。有关密度原则的一般信息,请参阅 Material Design 指南。