仅仅调整应用程序的外观是不够的,你还需要支持各种用户输入。鼠标和键盘引入了触控设备以外的输入类型,例如滚轮、右键单击、悬停交互、Tab 键遍历和键盘快捷键。

其中一些功能在 Material 组件上默认可用。但是,如果你创建了自定义组件,可能需要直接实现它们。

一些构成精心设计的应用程序的功能,也帮助使用辅助技术的用户。例如,除了是好的应用程序设计,一些功能,如 Tab 键遍历和键盘快捷键,对于使用辅助设备的用户至关重要。除了创建无障碍应用程序的标准建议外,本页还涵盖了创建既适应又无障碍的应用程序的信息。

自定义组件的滚轮

#

ScrollViewListView 等滚动组件默认支持滚轮,而且由于几乎所有可滚动的自定义组件都使用这些组件之一构建,因此它们也适用于这些组件。

如果你需要实现自定义滚动行为,可以使用 Listener 组件,它允许你自定义 UI 如何响应滚轮。

dart
return Listener(
  onPointerSignal: (event) {
    if (event is PointerScrollEvent) print(event.scrollDelta.dy);
  },
  child: ListView(),
);

Tab 键遍历和焦点交互

#

使用实体键盘的用户期望他们可以使用 Tab 键快速导航应用程序,而有运动或视觉障碍的用户通常完全依赖键盘导航。

Tab 交互有两个考虑因素:焦点如何在组件之间移动(称为遍历),以及当组件获得焦点时显示的视觉高亮。

大多数内置组件,如按钮和文本字段,默认支持遍历和高亮。如果你有自己的组件想包含在遍历中,可以使用 FocusableActionDetector 组件创建自己的控件。FocusableActionDetector 组件对于在一个组件中组合焦点、鼠标输入和快捷键非常有用。你可以创建一个检测器,它定义动作和键绑定,并提供处理焦点和悬停高亮的回调。

dart
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 键切换到提交按钮之前,先遍历表单中的所有字段。

dart
return Column(
  children: [
    FocusTraversalGroup(child: MyFormWithMultipleColumnsAndRows()),
    SubmitButton(),
  ],
);

Flutter 有几种内置的遍历组件和组的方式,默认为 ReadingOrderTraversalPolicy 类。这个类通常运行良好,但可以通过使用另一个预定义的 TraversalPolicy 类或创建自定义策略来修改。

键盘加速键

#

除了 Tab 键遍历,桌面和网络用户也习惯于将各种键盘快捷键绑定到特定动作。无论是用于快速删除的 Delete 键,还是用于新建文档的 Control+N,请务必考虑用户期望的不同加速键。键盘是一个强大的输入工具,所以尽量从中榨取尽可能多的效率。你的用户会感激的!

根据你的目标,Flutter 中可以通过几种方式实现键盘加速键。

如果你有一个像 TextFieldButton 这样已经有焦点节点的单个组件,你可以用 KeyboardListenerFocus 组件包裹它,并监听键盘事件。

dart
  @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 组件。

dart
// 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 添加全局监听器很简单。

dart
@override
void initState() {
  super.initState();
  HardwareKeyboard.instance.addHandler(_handleKey);
}

@override
void dispose() {
  HardwareKeyboard.instance.removeHandler(_handleKey);
  super.dispose();
}

要使用全局监听器检查按键组合,你可以使用 HardwareKeyboard.instance.logicalKeysPressed 集合。例如,以下方法可以检查是否按下了任何提供的键:

dart
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
  return keys
      .intersection(HardwareKeyboard.instance.logicalKeysPressed)
      .isNotEmpty;
}

将这两者结合起来,你可以在按下 Shift+N 时触发一个动作。

dart
bool _handleKey(KeyEvent event) {
  bool isShiftDown = isKeyDown({
    LogicalKeyboardKey.shiftLeft,
    LogicalKeyboardKey.shiftRight,
  });

  if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
    _createNewItem();
    return true;
  }

  return false;
}

使用静态监听器时需要注意一点:当用户在字段中输入或关联的组件被隐藏时,通常需要禁用它。与 ShortcutsKeyboardListener 不同,这是你的责任。当你为 Delete 绑定一个删除/退格加速键,但又有用户可能正在键入的子 TextFields 时,这一点尤为重要。

自定义组件的鼠标进入、退出和悬停

#

在桌面上,通常会改变鼠标光标来指示鼠标悬停内容的功能。例如,当你悬停在按钮上时,通常会看到一个手型光标;当你悬停在文本上时,会看到一个 I 型光标。

Flutter 的 Material 按钮处理标准按钮和文本光标的基本焦点状态。(一个显著的例外是,如果你更改 Material 按钮的默认样式以将 overlayColor 设置为透明。)

为应用程序中任何自定义按钮或手势检测器实现焦点状态。如果你更改了默认的 Material 按钮样式,请测试键盘焦点状态并在需要时自行实现。

要从自定义组件内部更改光标,请使用 MouseRegion

dart
// 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 也适用于创建自定义的鼠标悬停和划过效果。

dart
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 类可以轻松调整整个应用程序的视图密度,例如,在触摸设备上使按钮更大(因此更容易点击)。

当你更改 MaterialAppVisualDensity 时,支持它的 MaterialComponents 会动画调整它们的密度以匹配。默认情况下,水平和垂直密度都设置为 0.0,但你可以将密度设置为任何负值或正值。通过在不同密度之间切换,你可以轻松调整 UI。

Adaptive scaffold

要设置自定义视觉密度,请将密度注入到 MaterialApp 主题中。

dart
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,你可以查找它。

dart
VisualDensity density = Theme.of(context).visualDensity;

容器不仅能自动响应密度的变化,而且在密度变化时也会进行动画处理。这使得你的自定义组件与内置组件无缝连接,在整个应用程序中实现平滑的过渡效果。

如所示,VisualDensity 是无单位的,因此对于不同的视图可能意味着不同的含义。在以下示例中,1 个密度单位等于 6 像素,但这完全由你决定。它是无单位的这一事实使其非常通用,并且应该适用于大多数情况。

值得注意的是,Material 通常将每个视觉密度单位的值设置为约 4 个逻辑像素。有关支持的组件的更多信息,请参阅 VisualDensity API。有关密度原理的更多信息,请参阅 Material Design 指南