跳到主內容

使用者輸入與無障礙功能

真正具有適應性的應用還會處理使用者輸入方式的差異,並程式設計以幫助有輔助功能需求的人。

僅僅調整應用的外觀是不夠的,你還必須支援各種使用者輸入。滑鼠和鍵盤引入了觸控裝置上沒有的輸入型別,例如滾輪、右鍵單擊、懸停互動、標籤遍歷和鍵盤快捷鍵。

這些功能中的一些在 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 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(),
            ),
        ],
      ),
    );
  }
}

控制遍歷順序

#

要對使用者透過標籤瀏覽時小部件獲得焦點的順序進行更多控制,可以使用 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
// 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 鍵的刪除加速器時,但隨後有子 TextField 供使用者鍵入時,這一點尤其重要。

自定義小部件的滑鼠進入、退出和懸停

#

在桌面端,通常會更改滑鼠游標以指示滑鼠懸停在其上內容的的功能。例如,當你將滑鼠懸停在按鈕上時,通常會看到手形游標,當你將滑鼠懸停在文字上時,會看到 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 指南