使用者輸入與無障礙功能
僅僅調整應用程式的外觀是不夠的,你還必須支援各種使用者輸入。滑鼠和鍵盤引入了觸控裝置上沒有的輸入型別,例如滾輪、右鍵單擊、懸停互動、Tab 鍵遍歷和鍵盤快捷鍵。
其中一些功能在 Material 元件上預設工作。但是,如果你建立了自定義元件,你可能需要直接實現它們。
一些包含精心設計的應用程式的功能,也幫助使用輔助技術的人。例如,除了是良好的應用程式設計之外,一些功能(如 Tab 鍵遍歷和鍵盤快捷鍵)對於使用輔助裝置的使用者至關重要。除了建立無障礙應用程式的標準建議之外,本頁還介紹了建立既自適應又無障礙的應用程式的資訊。
自定義元件的滾輪
#像 ScrollView 或 ListView 這樣的滾動元件預設支援滾輪,並且由於幾乎每個可滾動自定義元件都是使用其中之一構建的,因此它也適用於這些元件。
如果你需要實現自定義滾動行為,你可以使用 Listener 元件,它允許你自定義 UI 如何響應滾輪。
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) print(event.scrollDelta.dy);
},
child: ListView(),
);Tab 鍵遍歷和焦點互動
#使用物理鍵盤的使用者希望他們可以使用 Tab 鍵快速導航應用程式,而有運動或視覺障礙的使用者通常完全依賴鍵盤導航。
Tab 鍵互動有兩個注意事項:焦點如何在元件之間移動(稱為遍歷)以及元件獲得焦點時顯示的視覺高亮。
大多數內建元件(例如按鈕和文字欄位)預設支援遍歷和高亮。如果你想將自己的元件包含在遍歷中,可以使用 FocusableActionDetector 元件來建立自己的控制元件。FocusableActionDetector 元件有助於將焦點、滑鼠輸入和快捷方式組合在一個元件中。你可以建立一個定義動作和按鍵繫結,並提供處理焦點和懸停高亮回撥的檢測器。
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 鍵切換到提交按鈕之前,先遍歷表單中的所有欄位。
return Column(
children: [
FocusTraversalGroup(child: MyFormWithMultipleColumnsAndRows()),
SubmitButton(),
],
);Flutter 有幾種內建的方法來遍歷元件和組,預設為 ReadingOrderTraversalPolicy 類。這個類通常工作得很好,但可以使用另一個預定義的 TraversalPolicy 類或透過建立自定義策略來修改它。
鍵盤快捷鍵
#除了 Tab 鍵遍歷,桌面和 Web 使用者習慣於將各種鍵盤快捷鍵繫結到特定操作。無論是用於快速刪除的 Delete 鍵,還是用於新建文件的 Control+N,請務必考慮使用者期望的不同快捷鍵。鍵盤是一個強大的輸入工具,所以儘量從中榨取儘可能高的效率。你的使用者會感謝你的!
根據你的目標,Flutter 中有幾種方法可以實現鍵盤快捷鍵。
如果你有一個像 TextField 或 Button 這樣的單個元件,它已經有一個焦點節點,你可以用 KeyboardListener 或 Focus 元件包裝它,並監聽鍵盤事件。
@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 元件。
// 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 新增全域性監聽器很容易。
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(_handleKey);
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKey);
super.dispose();
}要使用全域性監聽器檢查按鍵組合,可以使用 HardwareKeyboard.instance.logicalKeysPressed 集。例如,下面的方法可以檢查是否按下了任何提供的鍵。
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
return keys
.intersection(HardwareKeyboard.instance.logicalKeysPressed)
.isNotEmpty;
}將這兩者結合起來,你可以在按下 Shift+N 時觸發一個動作。
bool _handleKey(KeyEvent event) {
bool isShiftDown = isKeyDown({
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
});
if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
_createNewItem();
return true;
}
return false;
}使用靜態監聽器時需要注意一點,即當用戶在欄位中鍵入或與其關聯的元件從檢視中隱藏時,你通常需要停用它。與 Shortcuts 或 KeyboardListener 不同,這需要你自行管理。當你為 Delete 繫結一個刪除/退格加速器,但使用者可能正在子 TextFields 中鍵入時,這一點尤其重要。
自定義元件的滑鼠進入、離開和懸停
#在桌面端,通常會更改滑鼠游標以指示滑鼠懸停內容的功能。例如,當你懸停在按鈕上時,通常會看到手形游標;當你懸停在文字上時,通常會看到 I 形游標。
Flutter 的 Material 按鈕處理標準按鈕和文字游標的基本焦點狀態。(一個顯著的例外是,如果你更改 Material 按鈕的預設樣式以將 overlayColor 設定為透明。)
為應用程式中的任何自定義按鈕或手勢檢測器實現焦點狀態。如果你更改了預設的 Material 按鈕樣式,請測試鍵盤焦點狀態並在需要時實現你自己的焦點狀態。
要在自定義元件中更改游標,請使用 MouseRegion。
// 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 也適用於建立自定義的翻轉和懸停效果。
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 類可以輕鬆地調整整個應用程式中檢視的密度,例如,在觸控裝置上使按鈕更大(因此更容易點選)。
當你更改 MaterialApp 的 VisualDensity 時,支援它的 MaterialComponents 會將其密度調整為匹配。預設情況下,水平和垂直密度都設定為 0.0,但你可以將密度設定為任何負值或正值。透過在不同密度之間切換,你可以輕鬆調整 UI。

要設定自定義視覺密度,請將密度注入到 MaterialApp 主題中。
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,你可以查詢它。
VisualDensity density = Theme.of(context).visualDensity;容器不僅能自動響應密度的變化,而且在密度變化時也會進行動畫處理。這使得你的自定義元件與內建元件一起,在整個應用程式中實現平滑的過渡效果。
如所示,VisualDensity 是無單位的,因此它對不同的檢視可能意味著不同的東西。在以下示例中,1 個密度單位等於 6 畫素,但這完全由你決定。它是無單位的事實使其非常通用,並且應該適用於大多數情況。
值得注意的是,Material 通常為每個視覺密度單位使用大約 4 個邏輯畫素的值。有關支援元件的更多資訊,請參閱 VisualDensity API。有關密度原理的更多資訊,請參閱 Material Design 指南。