瞭解 Flutter 的鍵盤焦點系統
如何在你的 Flutter 應用中使用焦點系統。
本文件解釋瞭如何控制鍵盤輸入的方向。 如果你正在實現一個使用物理鍵盤的應用程式,例如大多數桌面和 Web 應用程式,那麼本頁內容對你來說很有用。 如果你的應用程式不會使用物理鍵盤,你可以跳過此頁。
概述
#Flutter 配備了一個焦點系統,可以將鍵盤輸入定向到應用程式的特定部分。 為了實現這一點,使用者透過點選或單擊所需的 UI 元素將輸入“聚焦”到應用程式的該部分。 一旦發生這種情況,透過鍵盤輸入的內容就會流向應用程式的該部分,直到焦點移動到應用程式的另一個部分。 焦點也可以透過按特定的鍵盤快捷鍵來移動,該快捷鍵通常繫結到 Tab 鍵,因此有時也稱為“選項卡遍歷”。
本頁探討了用於在 Flutter 應用程式上執行這些操作的 API,以及焦點系統的工作方式。 我們注意到,開發者們對如何定義和使用 FocusNode 物件存在一些困惑。 如果你的體驗如此,請跳到 建立 FocusNode 物件的最佳實踐。
焦點使用場景
#以下是一些你可能需要了解如何使用焦點系統的示例場景
術語表
#以下是 Flutter 使用的焦點系統元素的術語。 以下介紹了一些實現這些概念的類。
- 焦點樹 - 一個焦點節點樹,通常稀疏地映象小部件樹,表示可以接收焦點的所有小部件。
- 焦點節點 - 焦點樹中的單個節點。 該節點可以接收焦點,當它成為焦點鏈的一部分時,被稱為“擁有焦點”。 它僅在擁有焦點時才參與處理按鍵事件。
- 主焦點 - 從焦點樹的根到擁有焦點的最遠的焦點節點。 這是按鍵事件開始傳播到主焦點節點及其祖先的焦點節點。
- 焦點鏈 - 一個有序的焦點節點列表,從主焦點節點開始,沿著焦點樹的分支到焦點樹的根。
- 焦點範圍 - 一個特殊的焦點節點,其工作是包含其他焦點節點的組,並僅允許這些節點接收焦點。 它包含有關其子樹中先前聚焦的節點的的資訊。
- 焦點遍歷 - 在可預測的順序中從一個可聚焦節點移動到另一個可聚焦節點的過程。 這通常在應用程式中可以看到,當用戶按下 Tab 鍵移動到下一個可聚焦的控制元件或欄位時。
FocusNode 和 FocusScopeNode
#FocusNode 和 FocusScopeNode 物件實現了焦點系統的機制。 它們是長壽命物件(比小部件長,類似於渲染物件),儲存焦點狀態和屬性,以便在小部件樹的構建之間保持永續性。 它們共同構成焦點樹資料結構。
最初的設想是,它們是開發者面向的物件,用於控制焦點系統的一些方面,但隨著時間的推移,它們已經發展成為主要實現焦點系統細節。 為了防止破壞現有的應用程式,它們仍然包含其屬性的公共介面。 但是,通常來說,它們最有用的作用是充當相對不透明的控制代碼,傳遞給後代小部件,以便呼叫祖先小部件上的 requestFocus(),請求後代小部件獲得焦點。 其他屬性的設定最好由 Focus 或 FocusScope 部件管理,除非你不使用它們,或者實現你自己的版本。
建立 FocusNode 物件的最佳實踐
#關於使用這些物件的一些注意事項包括
- 不要為每次構建分配一個新的
FocusNode。 這可能會導致記憶體洩漏,並且偶爾會導致在小部件重建時失去焦點。 - 在有狀態小部件中建立
FocusNode和FocusScopeNode物件。FocusNode和FocusScopeNode需要在你完成使用它們時進行處置,因此它們應該只在有狀態小部件的狀態物件內建立,在那裡你可以覆蓋dispose來處置它們。 - 不要為多個小部件使用相同的
FocusNode。 如果這樣做,小部件將爭奪管理節點的屬性,你可能無法獲得預期的結果。 - 設定焦點小部件的
debugLabel以幫助診斷焦點問題。 - 如果它們由
Focus或FocusScope部件管理,請不要設定FocusNode或FocusScopeNode上的onKeyEvent回撥。 如果你需要一個onKeyEvent處理程式,則在希望偵聽的部件子樹周圍新增一個新的Focus部件,並將該部件的onKeyEvent屬性設定為你的處理程式。 如果你也不希望它能夠獲取主焦點,請在部件上設定canRequestFocus: false。 這是因為Focus部件上的onKeyEvent屬性可以在後續構建中設定為其他內容,如果發生這種情況,它將覆蓋你在節點上設定的onKeyEvent處理程式。 - 呼叫節點的
requestFocus()以請求它接收主焦點,尤其是在從傳遞了它擁有的節點的祖先小部件到你希望聚焦的後代小部件時。 - 使用
focusNode.requestFocus()。 呼叫FocusScope.of(context).requestFocus(focusNode)沒有必要。focusNode.requestFocus()方法是等效且效能更高的。
取消焦點
#有一個 API 可以告訴節點“放棄焦點”,名為 FocusNode.unfocus()。 雖然它確實從節點中刪除了焦點,但重要的是要意識到,實際上並沒有“取消所有節點焦點”之說。 如果節點未聚焦,則必須將焦點傳遞到其他地方,因為始終存在主焦點。 當節點呼叫 unfocus() 時接收焦點的節點是最近的 FocusScopeNode,或者該範圍內的先前聚焦的節點,具體取決於傳遞給 unfocus() 的 disposition 引數。 如果你希望更多地控制在從節點刪除焦點時焦點去向,請顯式地聚焦另一個節點,而不是呼叫 unfocus(),或者使用焦點遍歷機制使用 focusInDirection、nextFocus 或 previousFocus 方法在 FocusNode 上找到另一個節點。
呼叫 unfocus() 時,disposition 引數允許兩種取消聚焦模式:UnfocusDisposition.scope 和 UnfocusDisposition.previouslyFocusedChild。 預設值為 scope,它將焦點傳遞給最近的父焦點範圍。 這意味著如果焦點隨後使用 FocusNode.nextFocus 移動到下一個節點,它將從該範圍內的“第一個”可聚焦項開始。
previouslyFocusedChild disposition 將搜尋範圍以查詢先前聚焦的子項並在其上請求焦點。 如果沒有先前聚焦的子項,則它等效於 scope。
Focus 部件
#Focus 部件擁有並管理焦點節點,並且是焦點系統的核心。 它管理其擁有的焦點節點從焦點樹的附加和分離,管理焦點節點的屬性和回撥,並具有靜態函式以啟用對附加到小部件樹的焦點節點的發現。
在最簡單的形式下,將 Focus 部件包裝在部件子樹周圍,允許該部件子樹作為焦點遍歷過程的一部分或每當在傳遞給它的 FocusNode 上呼叫 requestFocus 時獲得焦點。 當與呼叫 requestFocus 的手勢檢測器結合使用時,它可以單擊或點選時獲得焦點。
你可能會將 FocusNode 物件傳遞給 Focus 部件進行管理,但如果你不這樣做,它會建立一個自己的物件。 建立你自己的 FocusNode 的主要原因是能夠從父小部件呼叫 requestFocus() 來控制焦點。 FocusNode 的大部分其他功能最好透過更改 Focus 部件本身的屬性來訪問。
Focus 部件用於 Flutter 自己的大多數控制元件來實現其焦點功能。
以下是一個示例,展示瞭如何使用 Focus 部件使自定義控制元件可聚焦。 它建立一個帶有對接收焦點的文字的容器。
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 部件的工作方式:它只是一個將此屬性設定為 true 的 Focus 部件。
自動聚焦
#設定 Focus 部件的 autofocus 屬性會告訴該部件在它所屬的焦點範圍被聚焦時請求焦點。如果多個部件設定了 autofocus,則接收焦點的部件是任意的,因此請嘗試僅在一個焦點範圍內的部件上設定它。
autofocus 屬性僅在節點所屬的範圍內尚不存在焦點的情況下才生效。
在屬於不同焦點範圍的兩個節點上設定 autofocus 屬性是明確定義的:當它們各自的範圍被聚焦時,它們各自成為聚焦的部件。
變更通知
#可以使用 Focus.onFocusChanged 回撥來獲取有關特定節點焦點狀態更改的通知。它會通知節點是否被新增到或從焦點鏈中移除,這意味著即使它不是主焦點,也會收到通知。如果您只想知道是否已接收主焦點,請檢查焦點節點上 hasPrimaryFocus 是否為 true。
獲取 FocusNode
#有時,獲取 Focus 部件的焦點節點以詢問其屬性會很有用。
要從 Focus 部件的祖先訪問焦點節點,請建立一個 FocusNode 並將其作為 focusNode 屬性傳遞給 Focus 部件。由於需要對其進行處置,因此您傳遞的焦點節點需要由狀態型部件擁有,因此不要每次構建時都建立一個。
如果您需要從 Focus 部件的後代訪問焦點節點,則可以呼叫 Focus.of(context) 來獲取給定上下文附近 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 部件
#FocusScope 部件是 Focus 部件的特殊版本,它管理 FocusScopeNode 而不是 FocusNode。FocusScopeNode 是焦點樹中的一個特殊節點,充當焦點樹中焦點節點的分組機制。除非顯式聚焦範圍外的節點,否則焦點遍歷保持在焦點範圍內。
焦點範圍還會跟蹤其子樹中當前焦點和已聚焦節點的歷史記錄。這樣,如果節點釋放焦點或在具有焦點的狀態下被移除,則可以將焦點返回到先前具有焦點的節點。
焦點範圍還充當返回焦點的位置,如果其後代沒有焦點。這使得焦點遍歷程式碼能夠為查詢下一個(或第一個)可聚焦控制元件提供起始上下文。
如果您聚焦焦點範圍節點,它首先嚐試聚焦其子樹中的當前或最近聚焦的節點,或者請求自動焦點的節點(如果有)。如果沒有這樣的節點,它將接收焦點。
FocusableActionDetector 部件
#FocusableActionDetector 部件是一個將 Actions、Shortcuts、MouseRegion 和 Focus 部件的功能組合在一起的部件,以建立一個檢測器,該檢測器定義操作和鍵繫結,並提供用於處理焦點和懸停高亮的回撥。這是 Flutter 控制元件實現所有這些控制元件方面的方式。它只是使用組成部件來實現的,因此如果您不需要所有功能,則可以使用您需要的功能,但它是一種將這些行為構建到自定義控制元件中的便捷方式。
控制焦點遍歷
#一旦應用程式具有聚焦能力,許多應用程式想要做的下一步就是允許使用者使用鍵盤或其他輸入裝置控制焦點。最常見的示例是“選項卡遍歷”,使用者按下 Tab 鍵轉到“下一個”控制元件。控制“下一個”的含義是本節的主題。Flutter 預設提供這種遍歷。
在簡單的網格佈局中,確定哪個控制元件是下一個控制元件相對容易。如果您不在行的末尾,則它是右側的控制元件(對於從右到左的語言環境,則是左側)。如果您在行的末尾,則是下一行的第一個控制元件。不幸的是,應用程式很少以網格佈局,因此通常需要更多指導。
Flutter 中的預設演算法(ReadingOrderTraversalPolicy)用於焦點遍歷非常好:它為大多數應用程式提供了正確的答案。但是,總會有病態情況,或者上下文或設計需要與預設排序演算法得出的不同順序的情況。對於這些情況,還有其他機制可以實現所需的順序。
FocusTraversalGroup 部件
#FocusTraversalGroup 部件應放置在樹中圍繞應完全遍歷後再移動到另一個部件或部件組的部件子樹周圍。僅將部件分組到相關組中通常足以解決許多選項卡遍歷排序問題。如果不是,該組還可以提供 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 回撥來監聽焦點高亮模式的變化。