使用 Actions 和 Shortcuts
本頁說明如何將實體鍵盤事件繫結到使用者介面中的動作。例如,要在您的應用程式中定義鍵盤快捷鍵,本頁適合您。
概觀
#為了使 GUI 應用程式執行任何操作,它必須具有 actions:使用者想要告訴應用程式做某些事情。Actions 通常是直接執行動作的簡單函式(例如設定值或儲存檔案)。但是,在較大的應用程式中,事情會變得更加複雜:調用 action 的程式碼和 action 本身的程式碼可能需要在不同的位置。快捷鍵(按鍵繫結)可能需要在不了解它們所調用的 actions 的層級進行定義。
這就是 Flutter 的 actions 和快捷鍵系統的用武之地。它允許開發人員定義 actions 來實現繫結到它們的 intents。在此上下文中,intent 是使用者希望執行的通用動作,而 Intent
類別的實例表示 Flutter 中的這些使用者 intents。Intent
可以是通用的,在不同的上下文中由不同的 actions 來實現。 Action
可以是一個簡單的回呼(如 CallbackAction
的情況),或是更複雜的內容,例如與整個復原/重做架構或其他邏輯整合。
Shortcuts
是通過按下按鍵或按鍵組合來啟動的按鍵繫結。按鍵組合位於一個表格中,其中包含其繫結的 intent。當 Shortcuts
小工具調用它們時,它會將其匹配的 intent 發送到 actions 子系統以進行實現。
為了說明 actions 和快捷鍵中的概念,本文建立了一個簡單的應用程式,該應用程式允許使用者使用按鈕和快捷鍵來選取和複製文字欄位中的文字。
為什麼要將 Actions 與 Intents 分開?
#您可能會想:為什麼不直接將按鍵組合對應到 action?為什麼要有 intents?這是因為將按鍵對應定義的位置(通常在較高的層級)和 action 定義的位置(通常在較低的層級)分開處理很有用,而且能夠將單個按鍵組合對應到應用程式中的預期操作,並使其自動適應在焦點上下文中實現該預期操作的 action,這一點非常重要。
例如,Flutter 有一個 ActivateIntent
小工具,可將每種類型的控制項對應到其對應版本的 ActivateAction
(並執行啟動控制項的程式碼)。此程式碼通常需要相當私有的存取權才能完成其工作。如果不存在 Intent
所提供的額外間接層,則必須將 actions 的定義提升到 Shortcuts
小工具的定義實例可以看到它們的位置,這會導致快捷鍵比實際需要更多地了解要調用的 action,並存取或提供它原本不必要或不需要的狀態。這允許您的程式碼將這兩個關注點分開,使其更加獨立。
Intents 配置 action,以便相同的 action 可以有多種用途。一個例子是 DirectionalFocusIntent
,它需要一個焦點移動的方向,允許 DirectionalFocusAction
知道要將焦點移動到哪個方向。請注意:不要在適用於所有 Action
調用的 Intent
中傳遞狀態:這類狀態應該傳遞到 Action
本身的建構函式中,以避免 Intent
需要了解太多資訊。
為什麼不使用回呼?
#您可能還想知道:為什麼不使用回呼來代替 Action
物件?主要原因是 actions 能夠通過實作 isEnabled
來決定它們是否已啟用,這很有用。此外,如果按鍵繫結和這些繫結的實作位於不同的位置,通常會很有幫助。
如果您只需要沒有 Actions
和 Shortcuts
的彈性的回呼,則可以使用 CallbackShortcuts
小工具。
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.arrowUp): () {
setState(() => count = count + 1);
},
const SingleActivator(LogicalKeyboardKey.arrowDown): () {
setState(() => count = count - 1);
},
},
child: Focus(
autofocus: true,
child: Column(
children: <Widget>[
const Text('Press the up arrow key to add to the counter'),
const Text('Press the down arrow key to subtract from the counter'),
Text('count: $count'),
],
),
),
);
}
快捷鍵
#正如您將在下面看到的,actions 本身很有用,但最常見的用例是將它們繫結到鍵盤快捷鍵。這就是 Shortcuts
小工具的用途。
它會插入小工具階層中,以定義按鍵組合,這些組合代表使用者在按下該按鍵組合時的意圖。為了將按鍵組合的預期用途轉換為具體的 action,使用 Actions
小工具將 Intent
對應到 Action
。例如,您可以定義一個 SelectAllIntent
,並將其繫結到您自己的 SelectAllAction
或 CanvasSelectAllAction
,並且從那一個按鍵繫結中,系統會調用任一個 action,這取決於應用程式的哪個部分具有焦點。讓我們看看按鍵繫結部分是如何運作的。
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
const SelectAllIntent(),
},
child: Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
onPressed: Actions.handler<SelectAllIntent>(
context,
const SelectAllIntent(),
),
child: const Text('SELECT ALL'),
),
),
),
);
}
提供給 Shortcuts
小工具的地圖會將 LogicalKeySet
(或 ShortcutActivator
,請參閱下面的註解)對應到 Intent
實例。邏輯按鍵集合定義一組或多個按鍵,而 intent 指示按下按鍵的預期用途。 Shortcuts
小工具會在該地圖中查詢按鍵,以尋找 Intent
實例,並將其提供給 action 的 invoke()
方法。
ShortcutManager
#快捷鍵管理器是一個比 Shortcuts
小工具更長壽命的物件,它會在收到按鍵事件時傳遞這些事件。它包含用於決定如何處理按鍵的邏輯,用於在樹狀結構中向上查找其他快捷鍵對應的邏輯,並維護按鍵組合到 intents 的地圖。
雖然通常需要 ShortcutManager
的預設行為,但 Shortcuts
小工具會採用您可以建立子類別以自訂其功能的 ShortcutManager
。
例如,如果您想記錄 Shortcuts
小工具處理的每個按鍵,則可以建立一個 LoggingShortcutManager
class LoggingShortcutManager extends ShortcutManager {
@override
KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
final KeyEventResult result = super.handleKeypress(context, event);
if (result == KeyEventResult.handled) {
print('Handled shortcut $event in $context');
}
return result;
}
}
現在,每次 Shortcuts
小工具處理快捷鍵時,它都會列印出按鍵事件和相關內容。
Actions
#Actions
允許定義應用程式可以透過使用 Intent
調用它們來執行的操作。可以啟用或停用 actions,並接收調用它們的 intent 實例作為引數,以便由 intent 進行設定。
定義 actions
#Actions 最簡單的形式只是具有 invoke()
方法的 Action<Intent>
子類別。這是一個簡單的 action,它只是在提供的模型上調用一個函式。
class SelectAllAction extends Action<SelectAllIntent> {
SelectAllAction(this.model);
final Model model;
@override
void invoke(covariant SelectAllIntent intent) => model.selectAll();
}
或者,如果建立新類別太麻煩,可以使用 CallbackAction
CallbackAction(onInvoke: (intent) => model.selectAll());
建立 action 後,您可以使用 Actions
小工具將其新增到應用程式中,該小工具會接收 Intent
類型到 Action
的地圖
@override
Widget build(BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: child,
);
}
Shortcuts
小工具會使用 Focus
小工具的上下文和 Actions.invoke
來尋找要調用的 action。如果 Shortcuts
小工具在遇到的第一個 Actions
小工具中找不到匹配的 intent 類型,它會考慮下一個父級 Actions
小工具,依此類推,直到它到達小工具樹狀結構的根目錄,或找到匹配的 intent 類型並調用相應的 action。
調用 Actions
#action 系統有多種調用 actions 的方式。到目前為止,最常見的方式是透過使用上一節中涵蓋的 Shortcuts
小工具,但還有其他方式可以查詢 action 子系統並調用 action。可以調用未繫結到按鍵的 actions。
例如,若要尋找與 intent 關聯的 action,您可以使用
Action<SelectAllIntent>? selectAll =
Actions.maybeFind<SelectAllIntent>(context);
如果指定的 context
中有可用的,則會傳回與 SelectAllIntent
類型關聯的 Action
。如果沒有可用的,則會傳回 null。如果應該始終提供關聯的 Action
,則使用 find
而不是 maybeFind
,當找不到匹配的 Intent
類型時,它會擲回例外狀況。
若要調用 action(如果存在),請呼叫
Object? result;
if (selectAll != null) {
result =
Actions.of(context).invokeAction(selectAll, const SelectAllIntent());
}
將其與下列內容結合到一個呼叫中
Object? result =
Actions.maybeInvoke<SelectAllIntent>(context, const SelectAllIntent());
有時您想要因按下按鈕或其他控制項而調用 action。您可以使用 Actions.handler
函式執行此操作。如果 intent 有對應到已啟用 action 的對應,則 Actions.handler
函式會建立處理程式閉包。但是,如果它沒有對應,則會傳回 null
。如果內容中沒有與之匹配的已啟用 action,這允許停用按鈕。
@override
Widget build(BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
onPressed: Actions.handler<SelectAllIntent>(
context,
SelectAllIntent(controller: controller),
),
child: const Text('SELECT ALL'),
),
),
);
}
只有當 isEnabled(Intent intent)
回傳 true 時,Actions
小工具才會呼叫動作,讓動作決定分派器是否應考慮呼叫它。如果動作未啟用,則 Actions
小工具會讓在小工具階層中較高的另一個已啟用動作(如果存在)有機會執行。
先前的範例使用 Builder
,因為 Actions.handler
和 Actions.invoke
(例如)只會在提供的 context
中尋找動作,如果範例將提供給 build
函數的 context
傳遞,框架會開始尋找目前小工具之上的動作。使用 Builder
可讓框架找到在同一個 build
函數中定義的動作。
您可以在不需要 BuildContext
的情況下呼叫動作,但是由於 Actions
小工具需要一個 context 才能找到要呼叫的已啟用動作,因此您需要提供一個 context,可以透過建立您自己的 Action
實例,或透過 Actions.find
在適當的 context 中找到一個。
要呼叫動作,請將動作傳遞給 ActionDispatcher
上的 invoke
方法,可以是您自己建立的,或使用 Actions.of(context)
方法從現有的 Actions
小工具中擷取的。在呼叫 invoke
之前,請檢查動作是否已啟用。當然,您也可以直接對動作本身呼叫 invoke
,並傳遞一個 Intent
,但這樣您就選擇不使用動作分派器可能提供的任何服務(例如記錄、還原/重做等)。
Action 分派器
#大多數情況下,您只想呼叫一個動作,讓它執行其功能,然後忘記它。但是,有時您可能想要記錄已執行的動作。
這就是使用自訂分派器取代預設 ActionDispatcher
的用武之地。您將您的 ActionDispatcher
傳遞給 Actions
小工具,它會從任何沒有設定自己分派器的下方 Actions
小工具中呼叫動作。
Actions
在呼叫動作時做的第一件事是查閱 ActionDispatcher
,並將動作傳遞給它以進行呼叫。如果沒有分派器,它會建立一個預設的 ActionDispatcher
,單純呼叫該動作。
但是,如果您想要記錄所有被呼叫的動作,您可以建立自己的 LoggingActionDispatcher
來完成此任務
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
@override
(bool, Object?) invokeActionIfEnabled(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
return super.invokeActionIfEnabled(action, intent, context);
}
}
然後您將其傳遞給最上層的 Actions
小工具
@override
Widget build(BuildContext context) {
return Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
onPressed: Actions.handler<SelectAllIntent>(
context,
const SelectAllIntent(),
),
child: const Text('SELECT ALL'),
),
),
);
}
這會記錄每個執行中的動作,如下所示
flutter: Action invoked: SelectAllAction#906fc(SelectAllIntent#a98e3) from Builder(dependencies: _[ActionsMarker])
整合在一起
#Actions
和 Shortcuts
的組合非常強大:您可以在小工具層級定義對應到特定動作的通用意圖。這是一個簡單的應用程式,說明了上述概念。該應用程式會建立一個文字欄位,其旁邊還有「全選」和「複製到剪貼簿」按鈕。這些按鈕會呼叫動作來完成其工作。所有被呼叫的動作和快捷鍵都會被記錄下來。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// A text field that also has buttons to select all the text and copy the
/// selected text to the clipboard.
class CopyableTextField extends StatefulWidget {
const CopyableTextField({super.key, required this.title});
final String title;
@override
State<CopyableTextField> createState() => _CopyableTextFieldState();
}
class _CopyableTextFieldState extends State<CopyableTextField> {
late final TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
ClearIntent: ClearAction(controller),
CopyIntent: CopyAction(controller),
SelectAllIntent: SelectAllAction(controller),
},
child: Builder(builder: (context) {
return Scaffold(
body: Center(
child: Row(
children: <Widget>[
const Spacer(),
Expanded(
child: TextField(controller: controller),
),
IconButton(
icon: const Icon(Icons.copy),
onPressed:
Actions.handler<CopyIntent>(context, const CopyIntent()),
),
IconButton(
icon: const Icon(Icons.select_all),
onPressed: Actions.handler<SelectAllIntent>(
context, const SelectAllIntent()),
),
const Spacer(),
],
),
),
);
}),
);
}
}
/// A ShortcutManager that logs all keys that it handles.
class LoggingShortcutManager extends ShortcutManager {
@override
KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
final KeyEventResult result = super.handleKeypress(context, event);
if (result == KeyEventResult.handled) {
print('Handled shortcut $event in $context');
}
return result;
}
}
/// An ActionDispatcher that logs all the actions that it invokes.
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
}
/// An intent that is bound to ClearAction in order to clear its
/// TextEditingController.
class ClearIntent extends Intent {
const ClearIntent();
}
/// An action that is bound to ClearIntent that clears its
/// TextEditingController.
class ClearAction extends Action<ClearIntent> {
ClearAction(this.controller);
final TextEditingController controller;
@override
Object? invoke(covariant ClearIntent intent) {
controller.clear();
return null;
}
}
/// An intent that is bound to CopyAction to copy from its
/// TextEditingController.
class CopyIntent extends Intent {
const CopyIntent();
}
/// An action that is bound to CopyIntent that copies the text in its
/// TextEditingController to the clipboard.
class CopyAction extends Action<CopyIntent> {
CopyAction(this.controller);
final TextEditingController controller;
@override
Object? invoke(covariant CopyIntent intent) {
final String selectedString = controller.text.substring(
controller.selection.baseOffset,
controller.selection.extentOffset,
);
Clipboard.setData(ClipboardData(text: selectedString));
return null;
}
}
/// An intent that is bound to SelectAllAction to select all the text in its
/// controller.
class SelectAllIntent extends Intent {
const SelectAllIntent();
}
/// An action that is bound to SelectAllAction that selects all text in its
/// TextEditingController.
class SelectAllAction extends Action<SelectAllIntent> {
SelectAllAction(this.controller);
final TextEditingController controller;
@override
Object? invoke(covariant SelectAllIntent intent) {
controller.selection = controller.selection.copyWith(
baseOffset: 0,
extentOffset: controller.text.length,
affinity: controller.selection.affinity,
);
return null;
}
}
/// The top level application class.
///
/// Shortcuts defined here are in effect for the whole app,
/// although different widgets may fulfill them differently.
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String title = 'Shortcuts and Actions Demo';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const ClearIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
const CopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
const SelectAllIntent(),
},
child: const CopyableTextField(title: title),
),
);
}
}
void main() => runApp(const MyApp());
除非另有說明,本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-06-26。 檢視原始碼 或 回報問題。