Skip to main content

用户输入和辅助功能

仅需适应应用程序的外观是不够的,您还必须支持各种用户输入。鼠标和键盘引入了触控设备上找不到的输入类型,例如滚轮、右键单击、悬停交互、选项卡遍历和键盘快捷键。

某些功能在 Material 小部件上默认有效。但是,如果您创建了自定义小部件,则可能需要直接实现它们。

一些包含精心设计的应用程序的功能也有助于使用辅助技术的用户。例如,除了是 良好的应用程序设计 之外,某些功能(例如选项卡遍历和键盘快捷键)对于 使用辅助设备的用户 至关重要。除了有关创建辅助功能应用程序 的标准建议外,本页面还介绍了创建既适应性强又易于访问的应用程序的信息。

自定义小部件的滚轮

#

ScrollViewListView这样的滚动小部件默认支持滚轮,并且由于几乎每个可滚动的自定义小部件都是使用其中一个构建的,因此它也适用于这些小部件。

如果您需要实现自定义滚动行为,可以使用Listener小部件,它允许您自定义UI对滚轮的反应方式。

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

选项卡遍历和焦点交互

#

使用物理键盘的用户期望他们可以使用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或Space键被按下!');
          return null;
        }),
      },
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          const FlutterLogo(size: 100),
          // 将焦点定位在负边距中以获得酷炫的效果
          if (_hasFocus)
            Positioned(
              left: -4,
              top: -4,
              bottom: -4,
              right: -4,
              child: _roundedBorder(),
            )
        ],
      ),
    );
  }
}

控制遍历顺序

#

为了更好地控制用户在其中遍历时聚焦小部件的顺序,您可以使用FocusTraversalGroup来定义在选项卡式浏览时应视为一个组的树的部分。

例如,您可能需要先遍历表单中的所有字段,然后再遍历到提交按钮:

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

Flutter 有几种内置方法可以遍历小部件和组,默认为 ReadingOrderTraversalPolicy 类。此类通常效果很好,但可以使用另一个预定义的 TraversalPolicy 类或通过创建自定义策略来修改它。

键盘快捷键

#

除了选项卡遍历外,桌面和 Web 用户习惯于将各种键盘快捷键绑定到特定操作。无论是用于快速删除的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
// 为您想要执行的每种类型的快捷键操作定义一个类
class CreateNewItemIntent extends Intent {
  const CreateNewItemIntent();
}

Widget build(BuildContext context) {
  return Shortcuts(
    // 将意图绑定到键组合
    shortcuts: const <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.keyN, control: true):
          CreateNewItemIntent(),
    },
    child: Actions(
      // 将意图绑定到代码中的实际方法
      actions: <Type, Action<Intent>>{
        CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
          onInvoke: (intent) => _createNewItem(),
        ),
      },
      // 您的子树必须包装在 focusNode 中,以便它可以获取焦点。
      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绑定Delete/Backspace快捷键,但又有用户可能在其中键入的子TextFields时,这一点尤其重要。

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

#

在桌面上,更改鼠标光标以指示鼠标悬停在其上的内容的功能是很常见的。例如,当您将鼠标悬停在按钮上时,通常会看到一个手形光标,当您将鼠标悬停在文本上时,会看到一个“I”形光标。

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

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

要在自定义小部件中更改光标,请使用MouseRegion

dart
// 显示手形光标
return MouseRegion(
  cursor: SystemMouseCursors.click,
  // 点击时请求焦点
  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属性以检查按钮是否具有焦点,如果具有焦点,则添加轮廓。 [Wonderous 应用的按钮代码]: https://github.com/gskinnerTeam/flutter-wonderous-app/blob/8a29d6709668980340b1b59c3d3588f123edd4d8/lib/ui/common/controls/buttons.dart#L143 [FocusNode.hasFocus]: https://api.flutter.dev/flutter/widgets/FocusNode/hasFocus.html

视觉密度

#

例如,您可能需要扩大小部件的“点击区域”以适应触摸屏。

不同的输入设备提供不同的精度级别,这需要不同大小的点击区域。Flutter 的VisualDensity类使您可以轻松调整整个应用程序中视图的密度,例如,通过在触摸设备上使按钮更大(因此更容易点击)。

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

自适应脚手架

要设置自定义视觉密度,请将密度注入到您的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 指南