了解 Flutter 的鍵盤焦點系統
- 概觀
- 詞彙表
- FocusNode 和 FocusScopeNode
- Focus Widget
- FocusScope Widget
- FocusableActionDetector Widget
- 控制焦點移動
- 焦點管理器
本文說明如何控制鍵盤輸入的方向。如果您正在實作使用實體鍵盤的應用程式(例如大多數桌面和網頁應用程式),此頁面適合您。如果您的應用程式不會與實體鍵盤一起使用,您可以跳過此頁面。
概觀
#Flutter 帶有一個焦點系統,可將鍵盤輸入導向應用程式的特定部分。為了實現這一點,使用者需要通過點擊或點擊所需的 UI 元素,將輸入「焦點」放在應用程式的該部分。一旦發生這種情況,用鍵盤輸入的文字將流向應用程式的該部分,直到焦點移到應用程式的另一部分。焦點也可以通過按下特定的鍵盤快捷鍵來移動,這通常綁定到 Tab 鍵,因此有時也稱為「Tab 鍵遍歷」。
本頁將探討用於在 Flutter 應用程式上執行這些操作的 API,以及焦點系統的工作方式。我們注意到開發人員對於如何定義和使用 FocusNode
物件感到有些困惑。如果這描述了您的經驗,請跳到 建立 FocusNode
物件的最佳實務。
焦點使用案例
#以下是一些您可能需要了解如何使用焦點系統的情況範例
詞彙表
#以下是 Flutter 使用的焦點系統元素的術語。下面將介紹實作其中一些概念的各種類別。
- 焦點樹 (Focus tree) - 一個焦點節點樹,通常稀疏地鏡像 Widget 樹,代表所有可以接收焦點的 Widget。
- 焦點節點 (Focus node) - 焦點樹中的單一節點。此節點可以接收焦點,當它成為焦點鏈的一部分時,表示它「擁有焦點」。只有在它擁有焦點時,它才會參與處理按鍵事件。
- 主要焦點 (Primary focus) - 焦點樹中離根節點最遠的焦點節點,它擁有焦點。這是按鍵事件開始傳播到主要焦點節點及其祖先的焦點節點。
- 焦點鏈 (Focus chain) - 一個焦點節點的有序列表,從主要焦點節點開始,沿著焦點樹的分支到焦點樹的根節點。
- 焦點範圍 (Focus scope) - 一個特殊的焦點節點,其工作是包含一組其他焦點節點,並僅允許這些節點接收焦點。它包含有關其子樹中先前聚焦的節點的資訊。
- 焦點遍歷 (Focus traversal) - 以可預測的順序從一個可聚焦節點移動到另一個可聚焦節點的過程。當使用者按下 Tab 鍵移動到下一個可聚焦的控制項或欄位時,通常會在應用程式中看到這種情況。
FocusNode 和 FocusScopeNode
#FocusNode
和 FocusScopeNode
物件實作了焦點系統的機制。它們是長時間存在的物件(比 Widget 更長,類似於渲染物件),它們保存焦點狀態和屬性,以便它們在 Widget 樹的建構之間保持持久性。它們共同構成了焦點樹資料結構。
它們最初旨在成為開發人員面對的物件,用於控制焦點系統的某些方面,但隨著時間的推移,它們已演變為主要實作焦點系統的細節。為了防止破壞現有的應用程式,它們仍然包含其屬性的公開介面。但是,一般而言,它們最有用的作用是充當相對不透明的控制代碼,傳遞給子 Widget,以便在祖先 Widget 上呼叫 requestFocus()
,這會請求子 Widget 取得焦點。除非您未使用它們或實作自己的版本,否則最好由 Focus
或 FocusScope
Widget 管理其他屬性的設定。
建立 FocusNode 物件的最佳實務
#以下是一些關於使用這些物件的注意事項
- 不要為每次建構分配新的
FocusNode
。這可能會導致記憶體洩漏,並且有時會在 Widget 重建時節點擁有焦點時導致焦點遺失。 - 請在狀態ful Widget 中建立
FocusNode
和FocusScopeNode
物件。FocusNode
和FocusScopeNode
在您完成使用它們時需要被釋放,因此它們應該只在狀態ful Widget 的狀態物件內建立,您可以在其中覆寫dispose
來釋放它們。 - 不要為多個 Widget 使用相同的
FocusNode
。如果您這樣做,Widget 將會爭奪管理節點的屬性,而您可能無法獲得預期的結果。 - 請設定焦點節點 Widget 的
debugLabel
,以協助診斷焦點問題。 - 如果
FocusNode
或FocusScopeNode
由Focus
或FocusScope
Widget 管理,請不要在其上設定onKeyEvent
回呼。如果您想要onKeyEvent
處理常式,請在您想要監聽的 Widget 子樹周圍新增一個新的Focus
Widget,並將 Widget 的onKeyEvent
屬性設定為您的處理常式。如果您也不希望它能夠取得主要焦點,請將 Widget 上的canRequestFocus
設定為false
。這是因為Focus
Widget 上的onKeyEvent
屬性可以在後續建構中設定為其他內容,如果發生這種情況,它會覆寫您在節點上設定的onKeyEvent
處理常式。 - 請在節點上呼叫
requestFocus()
,以請求該節點接收主要焦點,尤其是在已將其擁有的節點傳遞給您想要聚焦的子節點的祖先上。 - 請使用
focusNode.requestFocus()
。不需要呼叫FocusScope.of(context).requestFocus(focusNode)
。focusNode.requestFocus()
方法是等效的,而且效能更高。
取消焦點
#有一個 API 可讓節點「放棄焦點」,名為 FocusNode.unfocus()
。雖然它確實會從節點中移除焦點,但重要的是要意識到實際上沒有所謂「取消聚焦」所有節點的情況。如果一個節點取消聚焦,那麼它必須將焦點傳遞到其他地方,因為總是存在一個主要焦點。當節點呼叫 unfocus()
時接收焦點的節點,取決於給 unfocus()
的 disposition
引數,可以是最近的 FocusScopeNode
,也可以是該範圍中先前聚焦的節點。如果您希望對從節點移除焦點時焦點的去向有更多的控制,請明確聚焦另一個節點,而不是呼叫 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 本身的屬性來存取。
在 Flutter 的大多數控制項中,都使用了 Focus
Widget 來實作其焦點功能。
以下範例顯示如何使用 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
小部件的 onKeyEvent
屬性設定為一個處理程序,該處理程序可以僅監聽按鍵,或處理按鍵並阻止其傳播到其他小部件。
按鍵事件從具有主要焦點的焦點節點開始。如果該節點的 onKeyEvent
處理程序未返回 KeyEventResult.handled
,則會將事件傳遞給其父焦點節點。如果父節點沒有處理它,則會傳遞給其父節點,依此類推,直到到達焦點樹的根節點。如果事件到達焦點樹的根節點但未被處理,則會返回到平台,以便傳遞給應用程式中的下一個原生控制項 (以防 Flutter UI 是較大的原生應用程式 UI 的一部分)。已處理的事件不會傳播到其他 Flutter 小部件,也不會傳播到原生小部件。
以下是一個 Focus
小部件的範例,它會吸收其子樹未處理的每個按鍵,而本身無法成為主要焦點
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) => KeyEventResult.handled,
canRequestFocus: false,
child: child,
);
}
焦點按鍵事件會在文字輸入事件之前處理,因此,當焦點小部件包圍文字欄位時,處理按鍵事件會阻止該按鍵輸入到文字欄位中。
以下是一個小部件的範例,它不允許在文字欄位中輸入字母 "a"
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
return (event.logicalKey == LogicalKeyboardKey.keyA)
? KeyEventResult.handled
: KeyEventResult.ignored;
},
child: const TextField(),
);
}
如果意圖是輸入驗證,這個範例的功能可能會使用 TextInputFormatter
更好地實作,但這種技術仍然很有用:例如,Shortcuts
小部件會使用此方法在快捷鍵成為文字輸入之前處理它們。
控制取得焦點的項目
#焦點的主要方面之一是控制什麼可以接收焦點以及如何接收焦點。canRequestFocus
、skipTraversal
和 descendantsAreFocusable
屬性控制此節點及其後代如何參與焦點過程。
如果 skipTraversal
屬性為 true,則此焦點節點不會參與焦點遍歷。如果在其焦點節點上呼叫 requestFocus
,它仍然可以聚焦,但在焦點遍歷系統尋找下一個要聚焦的項目時,會跳過它。
不出所料,canRequestFocus
屬性控制是否可以使用此 Focus
小部件管理的焦點節點來請求焦點。如果此屬性為 false,則在節點上呼叫 requestFocus
無效。這也表示此節點會被跳過焦點遍歷,因為它無法請求焦點。
descendantsAreFocusable
屬性控制此節點的後代是否可以接收焦點,但仍然允許此節點接收焦點。此屬性可用於關閉整個小部件子樹的焦點功能。這就是 ExcludeFocus
小部件的工作方式:它只是一個設定了此屬性的 Focus
小部件。
自動對焦
#設定 Focus
小部件的 autofocus
屬性會告知小部件,在它所屬的焦點範圍第一次聚焦時請求焦點。如果多個小部件設定了 autofocus
,則隨機選擇一個接收焦點,因此請嘗試僅在每個焦點範圍內的一個小部件上設定它。
只有在節點所屬的範圍內沒有焦點時,autofocus
屬性才會生效。
在屬於不同焦點範圍的兩個節點上設定 autofocus
屬性是明確定義的:當其對應的範圍聚焦時,每個節點都會成為聚焦的小部件。
變更通知
#Focus.onFocusChanged
回呼可用於接收特定節點的焦點狀態已變更的通知。它會在節點新增至或從焦點鏈移除時通知,這表示即使它不是主要焦點,也會收到通知。如果您只想知道是否已收到主要焦點,請檢查焦點節點上的 hasPrimaryFocus
是否為 true。
取得 FocusNode
#有時,取得 Focus
小部件的焦點節點以查詢其屬性很有用。
若要從 Focus
小部件的祖先存取焦點節點,請建立並傳遞一個 FocusNode
作為 Focus
小部件的 focusNode
屬性。因為它需要被處置,因此您傳遞的焦點節點需要由有狀態的小部件擁有,因此不要在每次建置時都建立一個新的。
如果您需要從 Focus
小部件的後代存取焦點節點,您可以呼叫 Focus.of(context)
來取得與給定上下文最接近的 Focus
小部件的焦點節點。如果您需要在同一個建置函式中取得 Focus
小部件的 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);
},
),
);
}
時機
#焦點系統的一個細節是,當請求焦點時,它只會在目前建置階段完成後才會生效。這表示焦點變更總是會延遲一個影格,因為變更焦點可能會導致小部件樹的任意部分重建,包括目前請求焦點的小部件的祖先。由於後代無法弄髒其祖先,因此必須在影格之間發生,以便在下一個影格上進行任何必要的變更。
FocusScope Widget
#FocusScope
小部件是 Focus
小部件的特殊版本,它管理 FocusScopeNode
而不是 FocusNode
。FocusScopeNode
是焦點樹中的一個特殊節點,充當子樹中焦點節點的分組機制。除非明確聚焦範圍外的節點,否則焦點遍歷會保持在焦點範圍內。
焦點範圍還會追蹤其子樹中聚焦的目前焦點和節點的歷史記錄。這樣,如果節點釋放焦點或在具有焦點時被移除,則可以將焦點返回給先前具有焦點的節點。
如果沒有任何後代具有焦點,焦點範圍也會作為返回焦點的位置。這允許焦點遍歷程式碼具有開始上下文,以尋找下一個(或第一個)要移動的焦點控制項。
如果您聚焦焦點範圍節點,它會先嘗試聚焦其子樹中目前或最近聚焦的節點,或其子樹中請求自動聚焦的節點(如果有的話)。如果沒有這樣的節點,則它會自行接收焦點。
FocusableActionDetector Widget
#FocusableActionDetector
是一個結合了 Actions
、Shortcuts
、MouseRegion
和 Focus
小部件的功能的小部件,以建立一個偵測器,該偵測器會定義動作和按鍵繫結,並提供用於處理焦點和懸停醒目標示的回呼。這也是 Flutter 控制項用來實作控制項所有這些方面的方式。它只是使用構成小部件實作的,因此,如果您不需要其所有功能,您可以只使用您需要的那些,但這是一種方便的方法,將這些行為建置到您的自訂控制項中。
控制焦點移動
#一旦應用程式具有聚焦能力,許多應用程式接下來想要做的事情是允許使用者使用鍵盤或其他輸入裝置控制焦點。最常見的範例是「Tab 遍歷」,使用者按下 Tab 以移至「下一個」控制項。控制「下一個」的含義是本節的主題。這種遍歷預設由 Flutter 提供。
在簡單的網格佈局中,很容易決定哪個控制項是下一個。如果您不在列的末尾,則它是右側的控制項(或從右到左的地區設定為左側的控制項)。如果您在一列的末尾,則它是下一列中的第一個控制項。不幸的是,應用程式很少以網格方式佈局,因此通常需要更多指導。
Flutter 中用於焦點遍歷的預設演算法 (ReadingOrderTraversalPolicy
) 相當不錯:它為大多數應用程式提供了正確的答案。但是,總會有病態的情況,或者情境或設計需要與預設排序演算法得出的順序不同的情況。對於這些情況,還有其他機制可以實現所需的順序。
FocusTraversalGroup Widget
#FocusTraversalGroup
小部件應該放置在小部件子樹周圍的樹狀結構中,這些子樹應該在移動到另一個小部件或小部件組之前完全遍歷。僅將小部件分組為相關群組通常足以解決許多 Tab 遍歷順序問題。如果沒有,還可以為群組提供一個 FocusTraversalPolicy
,以確定群組內的順序。
預設的 ReadingOrderTraversalPolicy
通常足夠了,但在需要更多控制順序的情況下,可以使用 OrderedTraversalPolicy
。FocusTraversalOrder
小部件包裝在可聚焦元件周圍的 order
引數決定了順序。順序可以是 FocusOrder
的任何子類別,但提供了 NumericFocusOrder
和 LexicalFocusOrder
。
如果提供的焦點遍歷原則都不足以滿足您的應用程式,您也可以編寫自己的原則,並使用它來決定您想要的任何自訂排序。
以下範例說明如何使用 FocusTraversalOrder
小部件,使用 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
是決定哪個小部件是下一個的物件,給定一個請求和目前焦點節點。請求(成員函式)像是 findFirstFocus
、findLastFocus
、next
、previous
和 inDirection
。
FocusTraversalPolicy
是具體原則的抽象基底類別,例如 ReadingOrderTraversalPolicy
、OrderedTraversalPolicy
和 DirectionalFocusTraversalPolicyMixin
類別。
若要使用 FocusTraversalPolicy
,您需要將它提供給 FocusTraversalGroup
,後者會決定該原則將有效的 小部件子樹。類別的成員函式很少被直接呼叫:它們旨在由焦點系統使用。
焦點管理器
#FocusManager
維護系統的目前主要焦點。它只有少數對焦點系統的使用者有用的 API。一個是 FocusManager.instance.primaryFocus
屬性,它包含目前聚焦的焦點節點,也可以從全域 primaryFocus
欄位存取。
其他有用的屬性是 FocusManager.instance.highlightMode
和 FocusManager.instance.highlightStrategy
。這些屬性由需要在「觸控」模式和「傳統」(滑鼠和鍵盤)模式之間切換焦點醒目標示的小部件使用。當使用者使用觸控來導覽時,焦點醒目標示通常會隱藏,而當他們切換到滑鼠或鍵盤時,需要再次顯示焦點醒目標示,讓他們知道焦點在哪裡。hightlightStrategy
會告知焦點管理員如何解譯裝置使用模式的變更:它可以根據最近的輸入事件在兩者之間自動切換,也可以鎖定在觸控或傳統模式。Flutter 中提供的 小部件 已經知道如何使用此資訊,因此只有在您從頭開始編寫自己的控制項時才需要它。您可以使用 addHighlightModeListener
回呼來監聽醒目標示模式的變更。
除非另有說明,本網站上的文件均反映 Flutter 的最新穩定版本。頁面最後更新於 2024-07-06。 檢視原始碼 或 回報問題。