瞭解 Flutter 的鍵盤焦點系統
本文解釋如何控制鍵盤輸入的方向。如果你正在實現使用物理鍵盤的應用程式,例如大多數桌面和 Web 應用程式,本頁適合你。如果你的應用程式不與物理鍵盤一起使用,則可以跳過此內容。
概述
#Flutter 帶有一個焦點系統,用於將鍵盤輸入導向應用程式的特定部分。為此,使用者透過點選或單擊所需的 UI 元素將輸入“聚焦”到應用程式的該部分。一旦發生這種情況,使用鍵盤輸入的文字將流向應用程式的該部分,直到焦點移動到應用程式的另一個部分。焦點也可以透過按下特定的鍵盤快捷鍵來移動,該快捷鍵通常繫結到 Tab,因此有時也稱為“Tab 遍歷”。
本頁探討用於在 Flutter 應用程式上執行這些操作的 API,以及焦點系統的工作原理。我們注意到開發人員在如何定義和使用 FocusNode 物件方面存在一些困惑。如果這描述了你的經驗,請跳到建立 FocusNode 物件的最佳實踐。
焦點用例
#你需要了解如何使用焦點系統的一些情況示例
術語表
#以下是 Flutter 用於焦點系統元素的術語。實現其中一些概念的各種類將在下面介紹。
- 焦點樹 - 焦點節點的樹,通常稀疏地映象 widget 樹,表示所有可以接收焦點的 widget。
- 焦點節點 - 焦點樹中的單個節點。此節點可以接收焦點,當它成為焦點鏈的一部分時,據說它“具有焦點”。它僅在具有焦點時參與處理按鍵事件。
- 主要焦點 - 焦點樹中離焦點樹根最遠且具有焦點的焦點節點。這是按鍵事件開始傳播到主要焦點節點及其祖先的焦點節點。
- 焦點鏈 - 一個有序的焦點節點列表,從主要焦點節點開始,沿著焦點樹的分支到焦點樹的根。
- 焦點範圍 - 一個特殊的焦點節點,其作用是包含一組其他焦點節點,並只允許這些節點接收焦點。它包含有關其子樹中先前聚焦的節點的資訊。
- 焦點遍歷 - 以可預測的順序從一個可聚焦節點移動到另一個節點的過程。這通常在應用程式中出現,當用戶按下 Tab 鍵移動到下一個可聚焦控制元件或欄位時。
FocusNode 和 FocusScopeNode
#FocusNode 和 FocusScopeNode 物件實現了焦點系統的機制。它們是長生命週期的物件(比 widget 長,類似於渲染物件),它們儲存焦點狀態和屬性,以便在 widget 樹的多次構建之間保持永續性。它們共同構成了焦點樹資料結構。
它們最初旨在作為面向開發人員的物件,用於控制焦點系統的某些方面,但隨著時間的推移,它們已演變為主要實現焦點系統的細節。為了防止破壞現有應用程式,它們仍然包含其屬性的公共介面。但是,總的來說,它們最有用的是作為相對不透明的控制代碼,傳遞給後代 widget,以便在祖先 widget 上呼叫 requestFocus(),這會請求後代 widget 獲取焦點。除非你不使用它們或實現自己的版本,否則最好透過 Focus 或 FocusScope widget 來管理其他屬性的設定。
建立 FocusNode 物件的最佳實踐
#使用這些物件的一些注意事項包括
- 不要為每次構建分配新的
FocusNode。這可能導致記憶體洩漏,並且在節點具有焦點時 widget 重建時偶爾會導致焦點丟失。 - 在有狀態 widget 中建立
FocusNode和FocusScopeNode物件。FocusNode和FocusScopeNode在使用完後需要進行處置,因此它們應該只在有狀態 widget 的狀態物件中建立,你可以在其中覆蓋dispose來處置它們。 - 不要將同一個
FocusNode用於多個 widget。如果你這樣做,widget 將爭奪管理節點的屬性,你可能無法獲得預期結果。 - 設定焦點節點 widget 的
debugLabel以幫助診斷焦點問題。 - 如果
FocusNode或FocusScopeNode由Focus或FocusScopewidget 管理,請勿設定其onKeyEvent回撥。如果你想要一個onKeyEvent處理程式,那麼在你想要監聽的 widget 子樹周圍新增一個新的Focuswidget,並將該 widget 的onKeyEvent屬性設定為你的處理程式。如果你也不希望它能夠獲取主要焦點,請在 widget 上設定canRequestFocus: false。這是因為Focuswidget 上的onKeyEvent屬性可以在隨後的構建中設定為其他內容,如果發生這種情況,它會覆蓋你設定在節點上的onKeyEvent處理程式。 - 在節點上呼叫
requestFocus()以請求它接收主要焦點,特別是從將其擁有的節點傳遞給想要聚焦的後代節點的祖先。 - 使用
focusNode.requestFocus()。沒有必要呼叫FocusScope.of(context).requestFocus(focusNode)。focusNode.requestFocus()方法是等效的,效能更高。
取消焦點
#有一個 API 可以告訴節點“放棄焦點”,名為 FocusNode.unfocus()。雖然它確實從節點移除了焦點,但重要的是要認識到實際上沒有“取消聚焦”所有節點這樣的事情。如果一個節點被取消聚焦,那麼它必須將焦點傳遞到其他地方,因為總是有一個主要焦點。當一個節點呼叫 unfocus() 時接收焦點的節點要麼是最近的 FocusScopeNode,要麼是該範圍中先前聚焦的節點,具體取決於傳遞給 unfocus() 的 disposition 引數。如果你想更精細地控制從節點移除焦點時焦點的去向,請顯式地聚焦另一個節點而不是呼叫 unfocus(),或者使用焦點遍歷機制透過 FocusNode 上的 focusInDirection、nextFocus 或 previousFocus 方法查詢另一個節點。
呼叫 unfocus() 時,disposition 引數允許兩種取消焦點模式:UnfocusDisposition.scope 和 UnfocusDisposition.previouslyFocusedChild。預設是 scope,它將焦點交給最近的父級焦點範圍。這意味著如果此後焦點透過 FocusNode.nextFocus 移動到下一個節點,它將從範圍中的“第一個”可聚焦項開始。
previouslyFocusedChild 處置將搜尋範圍以找到先前聚焦的子級並請求對其進行聚焦。如果沒有先前聚焦的子級,則等同於 scope。
Focus widget
#Focus widget 擁有並管理一個焦點節點,並且是焦點系統的主力。它管理其擁有的焦點節點從焦點樹的附加和分離,管理焦點節點的屬性和回撥,並具有靜態函式以實現對附加到 widget 樹的焦點節點的發現。
以最簡單的形式,將 Focus widget 包裝在 widget 子樹周圍,允許該 widget 子樹作為焦點遍歷過程的一部分或在對其傳遞的 FocusNode 呼叫 requestFocus 時獲取焦點。與呼叫 requestFocus 的手勢檢測器結合使用時,它可以在點選或單擊時接收焦點。
你可能會將 FocusNode 物件傳遞給 Focus widget 進行管理,但如果你不這樣做,它會建立自己的。建立自己的 FocusNode 的主要原因是為了能夠在節點上呼叫 requestFocus() 以從父級 widget 控制焦點。FocusNode 的大多數其他功能最好透過更改 Focus widget 本身的屬性來訪問。
Focus widget 在 Flutter 自己的大多數控制元件中用於實現其焦點功能。
這是一個示例,展示瞭如何使用 Focus widget 使自定義控制元件可聚焦。它建立了一個帶有文字的容器,該文字對接收焦點做出反應。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Focus Sample';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[MyCustomWidget(), MyCustomWidget()],
),
),
);
}
}
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({super.key});
@override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget> {
Color _color = Colors.white;
String _label = 'Unfocused';
@override
Widget build(BuildContext context) {
return Focus(
onFocusChange: (focused) {
setState(() {
_color = focused ? Colors.black26 : Colors.white;
_label = focused ? 'Focused' : 'Unfocused';
});
},
child: Center(
child: Container(
width: 300,
height: 50,
alignment: Alignment.center,
color: _color,
child: Text(_label),
),
),
);
}
}按鍵事件
#如果你希望在子樹中監聽按鍵事件,請將 Focus widget 的 onKeyEvent 屬性設定為一個處理程式,該處理程式要麼只監聽按鍵,要麼處理按鍵並停止其向其他 widget 傳播。
按鍵事件從具有主要焦點的焦點節點開始。如果該節點沒有從其 onKeyEvent 處理程式返回 KeyEventResult.handled,則其父級焦點節點將獲得該事件。如果父級不處理它,它會轉到其父級,依此類推,直到到達焦點樹的根。如果事件到達焦點樹的根而未被處理,則它被返回到平臺以提供給應用程式中的下一個原生控制元件(以防 Flutter UI 是更大的原生應用程式 UI 的一部分)。已處理的事件不會傳播到其他 Flutter widget,也不會傳播到原生 widget。
這是一個 Focus widget 的示例,它吸收其子樹不處理的每個按鍵,而無需成為主要焦點
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) => KeyEventResult.handled,
canRequestFocus: false,
child: child,
);
}焦點按鍵事件在文字輸入事件之前處理,因此當焦點 widget 包圍文字欄位時處理按鍵事件會阻止該按鍵輸入到文字欄位中。
這是一個不允許字母“a”鍵入到文字欄位中的 widget 示例
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
return (event.logicalKey == LogicalKeyboardKey.keyA)
? KeyEventResult.handled
: KeyEventResult.ignored;
},
child: const TextField(),
);
}如果目的是輸入驗證,此示例的功能可能最好使用 TextInputFormatter 實現,但該技術仍然有用:例如,Shortcuts widget 使用此方法在快捷方式成為文字輸入之前處理它們。
控制焦點獲取
#焦點的一個主要方面是控制什麼可以接收焦點以及如何接收。屬性 canRequestFocus、skipTraversal 和 descendantsAreFocusable 控制此節點及其後代如何參與焦點過程。
如果 skipTraversal 屬性為 true,則此焦點節點不參與焦點遍歷。如果在其焦點節點上呼叫 requestFocus,它仍然可以聚焦,但在焦點遍歷系統尋找下一個要聚焦的東西時會被跳過。
canRequestFocus 屬性,不出所料,控制此 Focus widget 管理的焦點節點是否可以用於請求焦點。如果此屬性為 false,則在節點上呼叫 requestFocus 沒有效果。它還意味著此節點在焦點遍歷中被跳過,因為它無法請求焦點。
descendantsAreFocusable 屬性控制此節點的後代是否可以接收焦點,但仍允許此節點接收焦點。此屬性可用於關閉整個 widget 子樹的焦點能力。ExcludeFocus widget 就是這樣工作的:它只是一個設定了此屬性的 Focus widget。
自動獲取焦點
#設定 Focus widget 的 autofocus 屬性會告訴該 widget 在其所屬的焦點範圍首次聚焦時請求焦點。如果多個 widget 設定了 autofocus,則哪個接收焦點是任意的,因此請嘗試每個焦點範圍只在一個 widget 上設定它。
autofocus 屬性僅在節點所屬的範圍中尚未有焦點時才生效。
在屬於不同焦點範圍的兩個節點上設定 autofocus 屬性是明確定義的:當它們各自的範圍聚焦時,每個節點都成為聚焦的 widget。
更改通知
#Focus.onFocusChanged 回撥可用於獲取特定節點的焦點狀態已更改的通知。它會在節點新增到或從焦點鏈中移除時發出通知,這意味著即使它不是主要焦點,它也會收到通知。如果你只想知道是否已獲得主要焦點,請檢查焦點節點上的 hasPrimaryFocus 是否為 true。
獲取 FocusNode
#有時,獲取 Focus widget 的焦點節點以查詢其屬性很有用。
要從 Focus widget 的祖先訪問焦點節點,請建立並傳入一個 FocusNode 作為 Focus widget 的 focusNode 屬性。由於它需要被處置,你傳入的焦點節點需要由一個有狀態 widget 擁有,因此不要在每次構建時都建立一個。
如果你需要從 Focus widget 的後代訪問焦點節點,你可以呼叫 Focus.of(context) 來獲取給定上下文最近的 Focus widget 的焦點節點。如果你需要在同一個構建函式中獲取 Focus widget 的 FocusNode,請使用 Builder 來確保你擁有正確的上下文。這在以下示例中顯示
@override
Widget build(BuildContext context) {
return Focus(
child: Builder(
builder: (context) {
final bool hasPrimary = Focus.of(context).hasPrimaryFocus;
print('Building with primary focus: $hasPrimary');
return const SizedBox(width: 100, height: 100);
},
),
);
}時序
#焦點系統的一個細節是,當請求焦點時,它僅在當前構建階段完成後才生效。這意味著焦點更改總是延遲一幀,因為更改焦點可能導致 widget 樹的任意部分重建,包括當前請求焦點的 widget 的祖先。由於後代不能髒化其祖先,它必須在幀之間發生,以便任何必要的更改都可以在下一幀發生。
FocusScope widget
#FocusScope widget 是 Focus widget 的一個特殊版本,它管理一個 FocusScopeNode 而不是 FocusNode。FocusScopeNode 是焦點樹中的一個特殊節點,用作子樹中焦點節點的分組機制。焦點遍歷保留在焦點範圍內,除非明確聚焦範圍之外的節點。
焦點範圍還跟蹤其子樹中當前焦點和聚焦節點的歷史記錄。這樣,如果一個節點釋放焦點或在它有焦點時被移除,焦點可以返回到之前有焦點的節點。
如果沒有後代具有焦點,焦點範圍也充當返回焦點的地方。這允許焦點遍歷程式碼有一個起始上下文來查詢要移動到的下一個(或第一個)可聚焦控制元件。
如果你聚焦一個焦點範圍節點,它首先嚐試聚焦其子樹中當前或最近聚焦的節點,或者其子樹中請求自動聚焦的節點(如果有)。如果沒有這樣的節點,它會接收焦點本身。
FocusableActionDetector widget
#FocusableActionDetector 是一個 widget,它結合了 Actions、Shortcuts、MouseRegion 和 Focus widget 的功能,以建立一個定義動作和按鍵繫結並提供處理焦點和懸停高亮回撥的檢測器。它是 Flutter 控制元件用於實現這些控制元件所有方面的功能。它只是使用組成 widget 實現的,因此如果你不需要其所有功能,你可以只使用你需要的功能,但它是一種將這些行為構建到你的自定義控制元件中的便捷方式。
控制焦點遍歷
#一旦應用程式具有聚焦能力,許多應用程式接下來想要做的事情就是允許使用者使用鍵盤或其他輸入裝置控制焦點。最常見的例子是“Tab 遍歷”,使用者按下 Tab 鍵轉到“下一個”控制元件。控制“下一個”意味著什麼是本節的主題。這種遍歷是 Flutter 預設提供的。
在簡單的網格佈局中,很容易決定下一個控制元件是什麼。如果你不在行的末尾,那麼它就是右側的(或對於從右到左的區域設定,是左側的)。如果你在行的末尾,那麼它是下一行的第一個控制元件。不幸的是,應用程式很少以網格形式佈局,因此通常需要更多指導。
Flutter 中焦點遍歷的預設演算法(ReadingOrderTraversalPolicy)相當好:它對大多數應用程式都給出了正確答案。然而,總會有病態情況,或者上下文或設計需要與預設排序演算法得出的順序不同的情況。對於這些情況,還有其他機制可以實現所需的順序。
FocusTraversalGroup widget
#FocusTraversalGroup widget 應該放置在圍繞 widget 子樹的樹中,這些子樹應該在移動到另一個 widget 或 widget 組之前完全遍歷。僅僅將 widget 分組為相關組通常足以解決許多 Tab 遍歷排序問題。如果不能,該組還可以獲得一個 FocusTraversalPolicy 來確定組內的排序。
預設的 ReadingOrderTraversalPolicy 通常就足夠了,但在需要更多控制排序的情況下,可以使用 OrderedTraversalPolicy。圍繞可聚焦元件的 FocusTraversalOrder widget 的 order 引數決定了順序。順序可以是 FocusOrder 的任何子類,但提供了 NumericFocusOrder 和 LexicalFocusOrder。
如果提供的焦點遍歷策略都不足以滿足你的應用程式需求,你還可以編寫自己的策略並使用它來確定你想要的任何自定義排序。
這是一個如何使用 FocusTraversalOrder widget 使用 NumericFocusOrder 以 TWO、ONE、THREE 的順序遍歷一排按鈕的示例。
class OrderedButtonRow extends StatelessWidget {
const OrderedButtonRow({super.key});
@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: <Widget>[
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextButton(child: const Text('ONE'), onPressed: () {}),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextButton(child: const Text('TWO'), onPressed: () {}),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: TextButton(child: const Text('THREE'), onPressed: () {}),
),
const Spacer(),
],
),
);
}
}FocusTraversalPolicy
#FocusTraversalPolicy 是一個物件,它根據請求和當前焦點節點確定下一個 widget。請求(成員函式)包括 findFirstFocus、findLastFocus、next、previous 和 inDirection。
FocusTraversalPolicy 是具體策略(如 ReadingOrderTraversalPolicy、OrderedTraversalPolicy 和 DirectionalFocusTraversalPolicyMixin 類)的抽象基類。
為了使用 FocusTraversalPolicy,你需要將其提供給一個 FocusTraversalGroup,後者確定策略將生效的 widget 子樹。類的成員函式很少直接呼叫:它們旨在由焦點系統使用。
焦點管理器
#FocusManager 維護系統的當前主要焦點。它只有一些對焦點系統使用者有用的 API。其中之一是 FocusManager.instance.primaryFocus 屬性,它包含當前聚焦的焦點節點,也可以從全域性 primaryFocus 欄位訪問。
其他有用的屬性是 FocusManager.instance.highlightMode 和 FocusManager.instance.highlightStrategy。這些屬性由需要在其焦點高亮顯示中在“觸控”模式和“傳統”(滑鼠和鍵盤)模式之間切換的 widget 使用。當用戶使用觸控進行導航時,焦點高亮通常是隱藏的,當他們切換到滑鼠或鍵盤時,需要再次顯示焦點高亮,以便他們知道什麼被聚焦。highlightStrategy 告訴焦點管理器如何解釋裝置使用模式的變化:它可以根據最近的輸入事件自動在這兩種模式之間切換,或者可以鎖定在觸控或傳統模式中。Flutter 中提供的 widget 已經知道如何使用這些資訊,因此只有在你從頭開始編寫自己的控制元件時才需要它。你可以使用 addHighlightModeListener 回撥來監聽高亮模式的變化。