Focus系列的Widget及功能類在Flutter中能夠說是無名英雄的存在,默默的付出但卻不太爲人所知。在平常開發使用中也不太會用到它,這是爲何呢?帶着這個問題咱們開始今天的內容。java
這裏大體介紹一些Focus相關Widget及功能類,便於後面理解Focus Tree部分。本篇源碼基於1.20.0-2.0.pre。node
FocusNode
是用於Widget獲取鍵盤焦點和處理鍵盤事件的對象。它是繼承自ChangeNotifier
,因此咱們能夠在任意位置獲取對應的FocusNode
信息。linux
下面說幾個FocusNode
經常使用方法:android
requestFocus
用做請求焦點,注意這個請求焦點的執行放在了scheduleMicrotask
中,所以結果可能會延遲最多一幀。git
unfocus
用做取消焦點,默認行爲爲UnfocusDisposition.scope
:github
void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) {
....
}
複製代碼
UnfocusDisposition
枚舉類是焦點取消後的行爲,分爲scope
和previouslyFocusedChild
兩種。windows
scope
表示向上尋找最近的FocusScopeNode
。ide
previouslyFocusedChild
是尋找上一個焦點位置,若是沒有則給當前FocusScopeNode
。工具
具體實現可見unfocus
源碼,這裏就很少說了。ui
dispose
這個沒啥說的,注意使用FocusNode
完後及時銷燬。FocusScopeNode
是FocusNode
的子類。它將FocusNode
組織到一個做用域中,造成一組能夠遍歷的節點。它會提供最後一個獲取焦點的FocusNode
(focusedChild),若是其中一個節點的焦點被移除,那麼此FocusScopeNode
將再次得到焦點,同時_focusedChildren
清空。
/// Returns the child of this node that should receive focus if this scope
/// node receives focus.
///
/// If [hasFocus] is true, then this points to the child of this node that is
/// currently focused.
///
/// Returns null if there is no currently focused child.
FocusNode get focusedChild {
return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
}
// A stack of the children that have been set as the focusedChild, most recent
// last (which is the top of the stack).
final List<FocusNode> _focusedChildren = <FocusNode>[];
複製代碼
注意這裏的_focusedChildren
並非FocusScopeNode
下出現的全部FocusNode
,而是獲取過焦點的FocusNode
纔會在裏面。源碼實現以下:
void _setAsFocusedChildForScope() {
FocusNode scopeFocus = this;
for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
// 從聚焦的歷史中移除
ancestor._focusedChildren.remove(scopeFocus);
// 再將它添加至最後,這樣上面的focusedChild能夠獲取到最後獲取過焦點的節點
ancestor._focusedChildren.add(scopeFocus);
scopeFocus = ancestor;
}
}
複製代碼
FocusScopeNode
比較重要的方法是setFirstFocus
,用來設置子做用域節點。
void setFirstFocus(FocusScopeNode scope) {
if (scope._parent == null) {
// scope沒有父節點,將scope添加至當前節點下
_reparent(scope);
}
if (hasFocus) {
// 當前做用域存在焦點,_doRequestFocus將焦點移到scope上,同時記錄節點。
scope._doRequestFocus(findFirstFocus: true);
} else {
// 當前做用域不存在焦點,記錄節點。
scope._setAsFocusedChildForScope();
}
}
複製代碼
Focus
是一個Widget,能夠用來分配焦點給它自己及其子Widget。內部管理着一個FocusNode
,監聽焦點的變化,來保持焦點層次結構與Widget層次結構同步。
咱們經常使用的InkWell
就使用了它,而Button、 Chip等大量的Widget又使用了InkWell
,因此Focus
能夠說是無處不在。
咱們來看一下InkResponse
源碼:
Focus
,咱們看看它的
onFocusChange
實現:
void _handleFocusUpdate(bool hasFocus) {
_hasFocus = hasFocus;
_updateFocusHighlights();
if (widget.onFocusChange != null) {
widget.onFocusChange(hasFocus);
}
}
複製代碼
有焦點變化時修改_hasFocus
值調用_updateFocusHighlights
方法。
void _updateFocusHighlights() {
bool showFocus;
switch (FocusManager.instance.highlightMode) {
case FocusHighlightMode.touch:
showFocus = false;
break;
case FocusHighlightMode.traditional:
showFocus = _shouldShowFocus;
break;
}
updateHighlight(_HighlightType.focus, value: showFocus);
}
複製代碼
最終調用updateHighlight
方法讓WIdget有一個獲取焦點時的高亮顯示。
這裏有個枚舉類FocusHighlightMode
,它是表示使用何種交互模式獲取的焦點。分爲touch
和traditional
。
默認的區分實現以下:
static FocusHighlightMode get _defaultModeForPlatform {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
return FocusHighlightMode.traditional;
}
return FocusHighlightMode.touch;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return FocusHighlightMode.traditional;
}
return null;
}
複製代碼
移動端在沒有鼠標鏈接的狀況下都是touch
,桌面端都爲傳統的方式(鍵盤和鼠標)。
因此這也回答我一開始的問題,咱們通常只考慮了移動設備,也就是touch
的部分,這部分其實咱們不太須要給按鈕處理焦點效果,可能相似給Android TV盒子用的這類App才須要。而Flutter提供的Widget須要考慮各個平臺效果,因此才使用了這些。相似在上面的InkResponse
源碼中,還出現了MouseRegion
這個Widget,它是跟蹤鼠標移動的,好比在Web端鼠標移動到按鈕上,按鈕會有一個變化效果。
FocusScope
與Focus
相似,不過它的內部管理的是FocusScopeNode
。它不改變主焦點,它只是改變了接收焦點的做用域節點。這個在源碼中使用的很少,但卻都很重要的位置。
好比Navigator
和Route
,首先Navigator
有一個FocusScope
,自動獲取焦點。在它承載的一個個路由上也會添加FocusScope
,這樣當頁面跳轉/Dialog彈框時能夠將焦點的做用域移動到上面(經過setFirstFocus
方法)。
相似Drawer
也是同樣。當抽屜打開時,咱們的焦點做用域就要移動到Drawer
,因此也要使用FocusScope
。
若是咱們要管理焦點,在頁面中有一個Stack
,上層覆蓋了下層Widget致使下面不可操做。這時咱們就可使用FocusScope
將焦點做用域移動至上面。
Flutter裏面有按照分類不一樣存在各類各樣的「樹」,好比常說的三棵樹Widget Tree、Element Tree 和 RenderObject Tree,其餘的好比我以前博客說過的Semantics Tree,和這裏要介紹的Focus Tree。
Focus Tree是與Widget Tree獨立開的、結構相對簡單的樹,它是維護Widget Tree中可聚焦Widget之間的層次關係。Focus Tree由於沒法經過工具來可視化觀察,咱們可使用Focus Tree的管理類FocusManager
中的debugDumpFocusTree
方法打印出來。
因此這裏我新建一個項目,寫一個小例子來看一下。代碼很簡單,Column
裏一個TextField
和FlatButton
。
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Material(
child: Column(
children: [
TextField(),
FlatButton(
child: Text('打印FocusTree'),
onPressed: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
debugDumpFocusTree();
});
},
),
],
),
);
}
}
複製代碼
點擊按鈕,打印結果以下:
FocusManager#4148c
│ UPDATE SCHEDULED
│ primaryFocus: FocusScopeNode#af55c(_ModalScopeState<dynamic>
│ Focus Scope [PRIMARY FOCUS])
│ nextFocus: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS PATH])
│ primaryFocusCreator: FocusScope ← _ActionsMarker ← Actions ←
│ PageStorage ← Offstage ← _ModalScopeStatus ←
│ _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#bfb70]
│ ← _EffectiveTickerMode ← TickerMode ←
│ _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#3fa85]
│ ← _Theatre ← Overlay-[LabeledGlobalKey<OverlayState>#2d724] ←
│ _FocusMarker ← Semantics ← FocusScope ← AbsorbPointer ←
│ _PointerListener ← Listener ← HeroControllerScope ←
│ Navigator-[GlobalObjectKey<NavigatorState>
│ _WidgetsAppState#9404f] ← ⋯
│
└─rootScope: FocusScopeNode#185ad(Root Focus Scope [IN FOCUS PATH])
│ IN FOCUS PATH
│ focusedChildren: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS
│ PATH])
│
└─Child 1: FocusNode#5bacc(Shortcuts [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusNode#1cd76(FocusTraversalGroup [IN FOCUS PATH])
│ context: Focus
│ NOT FOCUSABLE
│ IN FOCUS PATH
│
└─Child 1: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS PATH])
│ context: FocusScope
│ IN FOCUS PATH
│
└─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])
│ context: FocusScope
│ PRIMARY FOCUS
│
├─Child 1: FocusNode#e72e2
│ context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
│
└─Child 2: FocusNode#0b7c0
context: Focus
複製代碼
我從下往上說一下表明的含義:
Child 1: FocusNode#e72e2
和Child 2: FocusNode#0b7c0
一看就是同級,表明的就是TextField
和FlatButton
。
上一層FocusScopeNode#af55c
是當前的頁面,能夠看到焦點目前在它上面(PRIMARY FOCUS
)。它是在 MaterialPageRoute
-> PageRoute
-> ModalRoute
->createOverlayEntries
-> _buildModalScope
方法,調用_ModalScope
建立的。
再上一層FocusScopeNode#4f0d5
是Navigator
,代碼以下:
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');
@override
Widget build(BuildContext context) {
return HeroControllerScope(
child: Listener(
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel,
child: AbsorbPointer(
absorbing: false,
child: FocusScope(
node: focusScopeNode, // <---
autofocus: true,
child: Overlay(
key: _overlayKey,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
),
),
),
),
);
}
複製代碼
WidgetsApp
的Shortcuts
和FocusTraversalGroup
建立的。rootScope
它是在WidgetsBinding
初始化時調用BuildOwner
建立FocusManager
而來的。mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override
void initInstances() {
super.initInstances();
_buildOwner = BuildOwner();
...
}
...
}
複製代碼
class BuildOwner {
/// Creates an object that manages widgets.
BuildOwner({ this.onBuildScheduled });
/// The object in charge of the focus tree.
FocusManager focusManager = FocusManager();
...
}
複製代碼
class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
FocusManager() {
rootScope._manager = this;
...
}
...
}
複製代碼
FocusManager
類的相關信息。primaryFocus
:當前的主焦點。rootScope
:當前Focus Tree的根節點。highlightMode
:當前獲取焦點的交互模式,上面有提到。highlightStrategy
:交互模式的策略,默認automatic
根據接收到的最後一種輸入方式,自動切換。也能夠指定使用某一種方式。FocusManager
也繼承自ChangeNotifier
,因此咱們能夠經過addListener
監聽primaryFocus
的變化。如今我先點擊一下輸入框,在點擊按鈕,打印結果以下(只取最後幾層):
primaryFocus: FocusNode#e72e2([PRIMARY FOCUS])
...
└─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
│ context: FocusScope
│ IN FOCUS PATH
│ focusedChildren: FocusNode#e72e2([PRIMARY FOCUS])
│
├─Child 1: FocusNode#e72e2([PRIMARY FOCUS])
│ context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
│ PRIMARY FOCUS
│
└─Child 2: FocusNode#0b7c0
context: Focus
複製代碼
能夠看到當前焦點primaryFocus
爲FocusNode#e72e2
也就是到了TextField
上。注意這裏的focusedChildren
此時只有FocusNode#e72e2
。
由於我點擊了TextField
,此時軟鍵盤彈出。如今我須要關閉軟鍵盤,我這裏有四種方法:
使用SystemChannels.textInput.invokeMethod('TextInput.hide')
方法,這種方法關閉軟鍵盤後焦點不變,還在TextField
上,因此有一個問題。好比這時你push到一個新的頁面再pop返回,此時軟鍵盤會再次彈出。這裏不推薦使用。
使用FocusScope.of(context).requestFocus(FocusNode())
方法,並打印一下Focus Tree
。
primaryFocus: FocusNode#7da34([PRIMARY FOCUS])
└─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
│ context: FocusScope
│ IN FOCUS PATH
│ focusedChildren: FocusNode#7da34([PRIMARY FOCUS]),
│ FocusNode#e72e2
│
├─Child 1: FocusNode#e72e2
│ context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
│
├─Child 2: FocusNode#0b7c0
│ context: Focus
└─Child 3: FocusNode#7da34([PRIMARY FOCUS])
PRIMARY FOCUS
複製代碼
能夠看到其實就在當前節點下建立了一個FocusNode#7da34
並把焦點轉移給它。注意這裏的focusedChildren
此時有FocusNode#7da34
和FocusNode#e72e2
。
FocusScope.of(context).unfocus()
方法重複上面的步驟,並打印一下Focus Tree
。primaryFocus: FocusScopeNode#4f0d5(Navigator Scope [PRIMARY FOCUS])
└─Child 1: FocusScopeNode#4f0d5(Navigator Scope [PRIMARY FOCUS])
│ context: FocusScope
│ PRIMARY FOCUS
│
└─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope)
│ context: FocusScope
│ focusedChildren: FocusNode#e72e2, FocusNode#7da34
│
├─Child 1: FocusNode#e72e2
│ context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
│
├─Child 2: FocusNode#0b7c0
│ context: Focus
└─Child 3: FocusNode#7da34
複製代碼
能夠看到焦點直接到了Navigator
上,爲何不是當前頁面FocusScopeNode#af55c
呢?
由於這裏FocusScope.of(context)
方法所返回的FocusScopeNode
就是當前頁面FocusScopeNode#af55c
,這時候你再取消了焦點,那麼焦點此時就向上尋找,到了Navigator
上。
注意這裏的focusedChildren
此時有FocusNode#e72e2
和FocusNode#7da34
。不過看到這裏你有沒有發現一個問題。焦點已經不在FocusScopeNode#af55c
的做用域裏面了,可是focusedChildren
裏卻還存在數據,若是咱們這時使用如FocusScope.of(context).focusedChild
方法,那麼獲得的結果就是不正確的。
穩妥的作法是使用下面的第四種方法。
TextField
添加屬性focusNode
,直接調用_focusNode.unfocus()
:final FocusNode _focusNode = FocusNode();
TextField(
focusNode: _focusNode,
),
_focusNode.unfocus();
複製代碼
這裏我就不貼結果了,大致和一開始的同樣,此時focusedChildren
爲空不打印。這樣就能夠將焦點成功歸還上級做用域(當前頁面),不過這樣若是頁面複雜,可能會比較繁瑣,你須要每一個添加FocusNode
來管理。因此更推薦使用:
FocusManager.instance.primaryFocus?.unfocus();
複製代碼
它能夠直接獲取到當前的焦點,便於咱們直接取消焦點。因此對比這四個方法,確定後者比較好了,也避免了因數據錯誤致使的其餘隱患。
經過觀察Focus Tree的變化,咱們大體能夠理解Focus Tree的組成及變化規律,若是你有控制焦點的需求,本篇或許能夠爲你帶來幫助。
關於Focus其實還有許多細節,好比FocusAttachment
如何管理FocusNode
、FocusNode
的遍歷順序實現 FocusTraversalGroup
等。因爲篇幅有限,這裏就不介紹了,有興趣的能夠看看源碼。
本篇是「說說」系列第四篇,前三篇連接奉上:
若是本文對你有所幫助或啓發的話,還請不吝點贊收藏支持一波。同時也多多支持個人Flutter開源項目flutter_deer。
咱們下個月見~~