使用者輸入與無障礙功能
一個真正自適應的應用程式,不僅要處理使用者輸入方式的差異,還要提供能夠幫助殘障人士的輔助功能。
僅僅調整應用程式的外觀是不夠的,你還必須支援多種使用者輸入方式。滑鼠和鍵盤引入了觸控裝置所沒有的輸入型別,例如滾輪、右鍵單擊、懸停互動、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 繫結刪除/退格快捷鍵,但同時又有使用者可能正在輸入的子 TextField 時,這一點尤為重要。
自定義元件的滑鼠進入、離開與懸停
#在桌面端,更改滑鼠游標以指示滑鼠懸停內容的功能是一種常見做法。例如,當你懸停在按鈕上時通常會看到手形游標,或者在懸停於文字上時看到 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 對於建立自定義的懸停(Rollover/Hover)效果也很有用。
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 指南。