Focus系列的Widget及功能類在Flutter中可以說是無名英雄的存在艘款,默默的付出但卻不太為人所知离熏。在日常開發(fā)使用中也不太會用到它杰扫,這是為什么呢凤壁?帶著這個問題我們開始今天的內(nèi)容吩屹。
1.Focus相關(guān)介紹
這里大致介紹一些Focus相關(guān)Widget及功能類,便于后面理解Focus Tree部分拧抖。本篇源碼基于1.20.0-2.0.pre煤搜。
1.1 FocusNode
FocusNode
是用于Widget獲取鍵盤焦點和處理鍵盤事件的對象。它是繼承自ChangeNotifier
唧席,所以我們可以在任意位置獲取對應(yīng)的FocusNode
信息擦盾。
下面說幾個FocusNode
常用方法:
requestFocus
用作請求焦點,注意這個請求焦點的執(zhí)行放在了scheduleMicrotask
中淌哟,因此結(jié)果可能會延遲最多一幀迹卢。unfocus
用作取消焦點,默認行為為UnfocusDisposition.scope
:
void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) {
....
}
UnfocusDisposition
枚舉類是焦點取消后的行為徒仓,分為scope
和previouslyFocusedChild
兩種腐碱。
scope
表示向上尋找最近的FocusScopeNode
。previouslyFocusedChild
是尋找上一個焦點位置掉弛,如果沒有則給當(dāng)前FocusScopeNode
症见。
具體實現(xiàn)可見unfocus
源碼,這里就不多說了殃饿。
-
dispose
這個沒啥說的谋作,注意使用FocusNode
完后及時銷毀。
1.2 FocusScopeNode
FocusScopeNode
是FocusNode
的子類壁晒。它將FocusNode
組織到一個作用域中瓷们,形成一組可以遍歷的節(jié)點。它會提供最后一個獲取焦點的FocusNode
(focusedChild)秒咐,如果其中一個節(jié)點的焦點被移除谬晕,那么此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
下出現(xiàn)的所有FocusNode
攒钳,而是獲取過焦點的FocusNode
才會在里面。源碼實現(xiàn)如下:
void _setAsFocusedChildForScope() {
FocusNode scopeFocus = this;
for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
// 從聚焦的歷史中移除
ancestor._focusedChildren.remove(scopeFocus);
// 再將它添加至最后雷滋,這樣上面的focusedChild可以獲取到最后獲取過焦點的節(jié)點
ancestor._focusedChildren.add(scopeFocus);
scopeFocus = ancestor;
}
}
FocusScopeNode
比較重要的方法是setFirstFocus
不撑,用來設(shè)置子作用域節(jié)點。
void setFirstFocus(FocusScopeNode scope) {
if (scope._parent == null) {
// scope沒有父節(jié)點晤斩,將scope添加至當(dāng)前節(jié)點下
_reparent(scope);
}
if (hasFocus) {
// 當(dāng)前作用域存在焦點焕檬,_doRequestFocus將焦點移到scope上,同時記錄節(jié)點澳泵。
scope._doRequestFocus(findFirstFocus: true);
} else {
// 當(dāng)前作用域不存在焦點实愚,記錄節(jié)點。
scope._setAsFocusedChildForScope();
}
}
1.3 Focus
Focus
是一個Widget,可以用來分配焦點給它本身及其子Widget腊敲。內(nèi)部管理著一個FocusNode
击喂,監(jiān)聽焦點的變化,來保持焦點層次結(jié)構(gòu)與Widget層次結(jié)構(gòu)同步碰辅。
我們常用的InkWell
就使用了它懂昂,而Button、 Chip等大量的Widget又使用了InkWell
没宾,所以Focus
可以說是無處不在凌彬。
我們來看一下InkResponse
源碼:
這里發(fā)現(xiàn)了
Focus
,我們看看它的onFocusChange
實現(xiàn):
void _handleFocusUpdate(bool hasFocus) {
_hasFocus = hasFocus;
_updateFocusHighlights();
if (widget.onFocusChange != null) {
widget.onFocusChange(hasFocus);
}
}
有焦點變化時修改_hasFocus
值調(diào)用_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);
}
最終調(diào)用updateHighlight
方法讓W(xué)Idget有一個獲取焦點時的高亮顯示饿序。
這里有個枚舉類FocusHighlightMode
,它是表示使用何種交互模式獲取的焦點羹蚣。分為touch
和traditional
原探。
默認的區(qū)分實現(xiàn)如下:
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
,桌面端都為傳統(tǒng)的方式(鍵盤和鼠標)顽素。
所以這也回答我一開始的問題咽弦,我們一般只考慮了移動設(shè)備,也就是touch
的部分胁出,這部分其實我們不太需要給按鈕處理焦點效果型型,可能類似給Android TV盒子用的這類App才需要。而Flutter提供的Widget需要考慮各個平臺效果全蝶,所以才使用了這些闹蒜。類似在上面的InkResponse
源碼中,還出現(xiàn)了MouseRegion
這個Widget抑淫,它是跟蹤鼠標移動的绷落,比如在Web端鼠標移動到按鈕上,按鈕會有一個變化效果始苇。
1.4 FocusScope
FocusScope
與Focus
類似砌烁,不過它的內(nèi)部管理的是FocusScopeNode
。它不改變主焦點催式,它只是改變了接收焦點的作用域節(jié)點函喉。這個在源碼中使用的不多,但卻都很重要的位置荣月。
比如Navigator
和Route
管呵,首先Navigator
有一個FocusScope
,自動獲取焦點哺窄。在它承載的一個個路由上也會添加FocusScope
撇寞,這樣當(dāng)頁面跳轉(zhuǎn)/Dialog彈框時可以將焦點的作用域移動到上面(通過setFirstFocus
方法)顿天。
類似Drawer
也是一樣堂氯。當(dāng)抽屜打開時蔑担,我們的焦點作用域就要移動到Drawer
,所以也要使用FocusScope
咽白。
如果我們要管理焦點啤握,在頁面中有一個Stack
,上層覆蓋了下層Widget導(dǎo)致下面不可操作晶框。這時我們就可以使用FocusScope
將焦點作用域移動至上面排抬。
2.Focus Tree
Flutter里面有按照分類不同存在各種各樣的“樹”,比如常說的三棵樹Widget Tree授段、Element Tree 和 RenderObject Tree蹲蒲,其他的比如我之前博客說過的Semantics Tree,和這里要介紹的Focus Tree侵贵。
Focus Tree是與Widget Tree獨立開的届搁、結(jié)構(gòu)相對簡單的樹,它是維護Widget Tree中可聚焦Widget之間的層次關(guān)系窍育。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();
});
},
),
],
),
);
}
}
點擊按鈕雷厂,打印結(jié)果如下:
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
是當(dāng)前的頁面弹沽,可以看到焦點目前在它上面(PRIMARY FOCUS
)。它是在
MaterialPageRoute
->PageRoute
->ModalRoute
->createOverlayEntries
->_buildModalScope
方法溶其,調(diào)用_ModalScope
創(chuàng)建的骚腥。再上一層
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
創(chuàng)建的瓶逃。
- 最頂層就是
rootScope
它是在WidgetsBinding
初始化時調(diào)用BuildOwner
創(chuàng)建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
類的相關(guān)信息。
-
primaryFocus
:當(dāng)前的主焦點厢绝。 -
rootScope
:當(dāng)前Focus Tree的根節(jié)點契沫。 -
highlightMode
:當(dāng)前獲取焦點的交互模式,上面有提到昔汉。 -
highlightStrategy
:交互模式的策略懈万,默認automatic
根據(jù)接收到的最后一種輸入方式,自動切換。也可以指定使用某一種方式会通。 -
FocusManager
也繼承自ChangeNotifier
口予,所以我們可以通過addListener
監(jiān)聽primaryFocus
的變化。
3.Focus Tree變化
現(xiàn)在我先點擊一下輸入框涕侈,在點擊按鈕沪停,打印結(jié)果如下(只取最后幾層):
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
可以看到當(dāng)前焦點primaryFocus
為FocusNode#e72e2
也就是到了TextField
上。注意這里的focusedChildren
此時只有FocusNode#e72e2
裳涛。
因為我點擊了TextField
木张,此時軟鍵盤彈出。現(xiàn)在我需要關(guān)閉軟鍵盤端三,我這里有四種方法:
使用
SystemChannels.textInput.invokeMethod('TextInput.hide')
方法舷礼,這種方法關(guān)閉軟鍵盤后焦點不變,還在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
可以看到其實就在當(dāng)前節(jié)點下創(chuàng)建了一個FocusNode#7da34
并把焦點轉(zhuǎn)移給它至朗。注意這里的focusedChildren
此時有FocusNode#7da34
和FocusNode#e72e2
。
- 使用
FocusScope.of(context).unfocus()
方法重復(fù)上面的步驟剧浸,并打印一下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
上,為什么不是當(dāng)前頁面FocusScopeNode#af55c
呢唆香?
因為這里FocusScope.of(context)
方法所返回的FocusScopeNode
就是當(dāng)前頁面FocusScopeNode#af55c
嫌变,這時候你再取消了焦點,那么焦點此時就向上尋找躬它,到了Navigator
上腾啥。
注意這里的focusedChildren
此時有FocusNode#e72e2
和FocusNode#7da34
。不過看到這里你有沒有發(fā)現(xiàn)一個問題冯吓。焦點已經(jīng)不在FocusScopeNode#af55c
的作用域里面了倘待,但是focusedChildren
里卻還存在數(shù)據(jù),如果我們這時使用如FocusScope.of(context).focusedChild
方法组贺,那么得到的結(jié)果就是不正確的凸舵。
穩(wěn)妥的做法是使用下面的第四種方法。
- 最后一個方法就是給
TextField
添加屬性focusNode
失尖,直接調(diào)用_focusNode.unfocus()
:
final FocusNode _focusNode = FocusNode();
TextField(
focusNode: _focusNode,
),
_focusNode.unfocus();
這里我就不貼結(jié)果了啊奄,大體和一開始的一樣渐苏,此時focusedChildren
為空不打印。這樣就可以將焦點成功歸還上級作用域(當(dāng)前頁面)菇夸,不過這樣如果頁面復(fù)雜琼富,可能會比較繁瑣,你需要每個添加FocusNode
來管理峻仇。所以更推薦使用:
FocusManager.instance.primaryFocus?.unfocus();
它可以直接獲取到當(dāng)前的焦點公黑,便于我們直接取消焦點。所以對比這四個方法摄咆,肯定后者比較好了,也避免了因數(shù)據(jù)錯誤導(dǎo)致的其他隱患人断。
4.結(jié)語
通過觀察Focus Tree的變化吭从,我們大致可以理解Focus Tree的組成及變化規(guī)律,如果你有控制焦點的需求恶迈,本篇或許可以為你帶來幫助涩金。
關(guān)于Focus其實還有許多細節(jié),比如FocusAttachment
如何管理FocusNode
暇仲、FocusNode
的遍歷順序?qū)崿F(xiàn) FocusTraversalGroup
等步做。由于篇幅有限,這里就不介紹了奈附,有興趣的可以看看源碼全度。
本篇是“說說”系列第四篇,前三篇鏈接奉上:
如果本文對你有所幫助或啟發(fā)的話斥滤,還請不吝點贊收藏支持一波将鸵。同時也多多支持我的Flutter開源項目flutter_deer。
我們下個月見~~