僅僅調整應用程式的外觀是不夠的,你還必須支援各種使用者輸入。滑鼠和鍵盤引入了觸控裝置上沒有的輸入型別,例如滾輪、右鍵單擊、懸停互動、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 鍵遍歷,桌面和 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
// 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 指南