跳至主要內容

使用者輸入 & 無障礙功能

僅調整應用程式的外觀是不夠的,您還必須支援各種使用者輸入。滑鼠和鍵盤引入了觸控裝置以外的輸入類型,例如滾輪、右鍵點擊、懸停互動、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 指南