Focus系列的Widget及功能類在Flutter中可以說(shuō)是無(wú)名英雄的存在,默默的付出但卻不太為人所知。在日常開(kāi)發(fā)使用中也不太會(huì)用到它,這是為什么呢?帶著這個(gè)問(wèn)題我們開(kāi)始今天的內(nèi)容。
1.Focus相關(guān)介紹
這里大致介紹一些Focus相關(guān)Widget及功能類,便于后面理解Focus Tree部分。本篇源碼基于1.20.0-2.0.pre。
1.1 FocusNode
FocusNode是用于Widget獲取鍵盤(pán)焦點(diǎn)和處理鍵盤(pán)事件的對(duì)象。它是繼承自ChangeNotifier,所以我們可以在任意位置獲取對(duì)應(yīng)的FocusNode信息。
下面說(shuō)幾個(gè)FocusNode常用方法:
requestFocus用作請(qǐng)求焦點(diǎn),注意這個(gè)請(qǐng)求焦點(diǎn)的執(zhí)行放在了scheduleMicrotask中,因此結(jié)果可能會(huì)延遲最多一幀。unfocus用作取消焦點(diǎn),默認(rèn)行為為UnfocusDisposition.scope:
void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) {
....
}
UnfocusDisposition枚舉類是焦點(diǎn)取消后的行為,分為scope和previouslyFocusedChild兩種。
scope表示向上尋找最近的FocusScopeNode。previouslyFocusedChild是尋找上一個(gè)焦點(diǎn)位置,如果沒(méi)有則給當(dāng)前FocusScopeNode。
具體實(shí)現(xiàn)可見(jiàn)unfocus源碼,這里就不多說(shuō)了。
-
dispose這個(gè)沒(méi)啥說(shuō)的,注意使用FocusNode完后及時(shí)銷毀。
1.2 FocusScopeNode
FocusScopeNode是FocusNode的子類。它將FocusNode組織到一個(gè)作用域中,形成一組可以遍歷的節(jié)點(diǎn)。它會(huì)提供最后一個(gè)獲取焦點(diǎn)的FocusNode(focusedChild),如果其中一個(gè)節(jié)點(diǎn)的焦點(diǎn)被移除,那么此FocusScopeNode將再次獲得焦點(diǎn),同時(shí)_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,而是獲取過(guò)焦點(diǎn)的FocusNode才會(huì)在里面。源碼實(shí)現(xiàn)如下:
void _setAsFocusedChildForScope() {
FocusNode scopeFocus = this;
for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
// 從聚焦的歷史中移除
ancestor._focusedChildren.remove(scopeFocus);
// 再將它添加至最后,這樣上面的focusedChild可以獲取到最后獲取過(guò)焦點(diǎn)的節(jié)點(diǎn)
ancestor._focusedChildren.add(scopeFocus);
scopeFocus = ancestor;
}
}
FocusScopeNode比較重要的方法是setFirstFocus,用來(lái)設(shè)置子作用域節(jié)點(diǎn)。
void setFirstFocus(FocusScopeNode scope) {
if (scope._parent == null) {
// scope沒(méi)有父節(jié)點(diǎn),將scope添加至當(dāng)前節(jié)點(diǎn)下
_reparent(scope);
}
if (hasFocus) {
// 當(dāng)前作用域存在焦點(diǎn),_doRequestFocus將焦點(diǎn)移到scope上,同時(shí)記錄節(jié)點(diǎn)。
scope._doRequestFocus(findFirstFocus: true);
} else {
// 當(dāng)前作用域不存在焦點(diǎn),記錄節(jié)點(diǎn)。
scope._setAsFocusedChildForScope();
}
}
1.3 Focus
Focus是一個(gè)Widget,可以用來(lái)分配焦點(diǎn)給它本身及其子Widget。內(nèi)部管理著一個(gè)FocusNode,監(jiān)聽(tīng)焦點(diǎn)的變化,來(lái)保持焦點(diǎn)層次結(jié)構(gòu)與Widget層次結(jié)構(gòu)同步。
我們常用的InkWell就使用了它,而B(niǎo)utton、 Chip等大量的Widget又使用了InkWell,所以Focus可以說(shuō)是無(wú)處不在。
我們來(lái)看一下InkResponse源碼:
這里發(fā)現(xiàn)了
Focus,我們看看它的onFocusChange實(shí)現(xiàn):
void _handleFocusUpdate(bool hasFocus) {
_hasFocus = hasFocus;
_updateFocusHighlights();
if (widget.onFocusChange != null) {
widget.onFocusChange(hasFocus);
}
}
有焦點(diǎn)變化時(shí)修改_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有一個(gè)獲取焦點(diǎn)時(shí)的高亮顯示。
這里有個(gè)枚舉類FocusHighlightMode,它是表示使用何種交互模式獲取的焦點(diǎn)。分為touch和traditional。
默認(rèn)的區(qū)分實(shí)現(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;
}
移動(dòng)端在沒(méi)有鼠標(biāo)連接的情況下都是touch,桌面端都為傳統(tǒng)的方式(鍵盤(pán)和鼠標(biāo))。
所以這也回答我一開(kāi)始的問(wèn)題,我們一般只考慮了移動(dòng)設(shè)備,也就是touch的部分,這部分其實(shí)我們不太需要給按鈕處理焦點(diǎn)效果,可能類似給Android TV盒子用的這類App才需要。而Flutter提供的Widget需要考慮各個(gè)平臺(tái)效果,所以才使用了這些。類似在上面的InkResponse源碼中,還出現(xiàn)了MouseRegion這個(gè)Widget,它是跟蹤鼠標(biāo)移動(dòng)的,比如在Web端鼠標(biāo)移動(dòng)到按鈕上,按鈕會(huì)有一個(gè)變化效果。
1.4 FocusScope
FocusScope與Focus類似,不過(guò)它的內(nèi)部管理的是FocusScopeNode。它不改變主焦點(diǎn),它只是改變了接收焦點(diǎn)的作用域節(jié)點(diǎn)。這個(gè)在源碼中使用的不多,但卻都很重要的位置。
比如Navigator和Route,首先Navigator有一個(gè)FocusScope,自動(dòng)獲取焦點(diǎn)。在它承載的一個(gè)個(gè)路由上也會(huì)添加FocusScope,這樣當(dāng)頁(yè)面跳轉(zhuǎn)/Dialog彈框時(shí)可以將焦點(diǎn)的作用域移動(dòng)到上面(通過(guò)setFirstFocus方法)。
類似Drawer也是一樣。當(dāng)抽屜打開(kāi)時(shí),我們的焦點(diǎn)作用域就要移動(dòng)到Drawer,所以也要使用FocusScope。
如果我們要管理焦點(diǎn),在頁(yè)面中有一個(gè)Stack,上層覆蓋了下層Widget導(dǎo)致下面不可操作。這時(shí)我們就可以使用FocusScope將焦點(diǎn)作用域移動(dòng)至上面。
2.Focus Tree
Flutter里面有按照分類不同存在各種各樣的“樹(shù)”,比如常說(shuō)的三棵樹(shù)Widget Tree、Element Tree 和 RenderObject Tree,其他的比如我之前博客說(shuō)過(guò)的Semantics Tree,和這里要介紹的Focus Tree。
Focus Tree是與Widget Tree獨(dú)立開(kāi)的、結(jié)構(gòu)相對(duì)簡(jiǎn)單的樹(shù),它是維護(hù)Widget Tree中可聚焦Widget之間的層次關(guān)系。Focus Tree因?yàn)闊o(wú)法通過(guò)工具來(lái)可視化觀察,我們可以使用Focus Tree的管理類FocusManager中的debugDumpFocusTree方法打印出來(lái)。
所以這里我新建一個(gè)項(xiàng)目,寫(xiě)一個(gè)小例子來(lái)看一下。代碼很簡(jiǎn)單,Column里一個(gè)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();
});
},
),
],
),
);
}
}
點(diǎn)擊按鈕,打印結(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
我從下往上說(shuō)一下代表的含義:
Child 1: FocusNode#e72e2和Child 2: FocusNode#0b7c0一看就是同級(jí),代表的就是TextField和FlatButton。上一層
FocusScopeNode#af55c是當(dāng)前的頁(yè)面,可以看到焦點(diǎn)目前在它上面(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初始化時(shí)調(diào)用BuildOwner創(chuàng)建FocusManager而來(lái)的。
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)前的主焦點(diǎn)。 -
rootScope:當(dāng)前Focus Tree的根節(jié)點(diǎn)。 -
highlightMode:當(dāng)前獲取焦點(diǎn)的交互模式,上面有提到。 -
highlightStrategy:交互模式的策略,默認(rèn)automatic根據(jù)接收到的最后一種輸入方式,自動(dòng)切換。也可以指定使用某一種方式。 -
FocusManager也繼承自ChangeNotifier,所以我們可以通過(guò)addListener監(jiān)聽(tīng)primaryFocus的變化。
3.Focus Tree變化
現(xiàn)在我先點(diǎn)擊一下輸入框,在點(diǎ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)前焦點(diǎn)primaryFocus為FocusNode#e72e2也就是到了TextField上。注意這里的focusedChildren此時(shí)只有FocusNode#e72e2。
因?yàn)槲尹c(diǎn)擊了TextField,此時(shí)軟鍵盤(pán)彈出?,F(xiàn)在我需要關(guān)閉軟鍵盤(pán),我這里有四種方法:
使用
SystemChannels.textInput.invokeMethod('TextInput.hide')方法,這種方法關(guān)閉軟鍵盤(pán)后焦點(diǎn)不變,還在TextField上,所以有一個(gè)問(wèn)題。比如這時(shí)你push到一個(gè)新的頁(yè)面再pop返回,此時(shí)軟鍵盤(pán)會(huì)再次彈出。這里不推薦使用。使用
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
可以看到其實(shí)就在當(dāng)前節(jié)點(diǎn)下創(chuàng)建了一個(gè)FocusNode#7da34并把焦點(diǎn)轉(zhuǎn)移給它。注意這里的focusedChildren此時(shí)有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
可以看到焦點(diǎn)直接到了Navigator上,為什么不是當(dāng)前頁(yè)面FocusScopeNode#af55c呢?
因?yàn)檫@里FocusScope.of(context)方法所返回的FocusScopeNode就是當(dāng)前頁(yè)面FocusScopeNode#af55c,這時(shí)候你再取消了焦點(diǎn),那么焦點(diǎn)此時(shí)就向上尋找,到了Navigator上。
注意這里的focusedChildren此時(shí)有FocusNode#e72e2和FocusNode#7da34。不過(guò)看到這里你有沒(méi)有發(fā)現(xiàn)一個(gè)問(wèn)題。焦點(diǎn)已經(jīng)不在FocusScopeNode#af55c的作用域里面了,但是focusedChildren里卻還存在數(shù)據(jù),如果我們這時(shí)使用如FocusScope.of(context).focusedChild方法,那么得到的結(jié)果就是不正確的。
穩(wěn)妥的做法是使用下面的第四種方法。
- 最后一個(gè)方法就是給
TextField添加屬性focusNode,直接調(diào)用_focusNode.unfocus():
final FocusNode _focusNode = FocusNode();
TextField(
focusNode: _focusNode,
),
_focusNode.unfocus();
這里我就不貼結(jié)果了,大體和一開(kāi)始的一樣,此時(shí)focusedChildren為空不打印。這樣就可以將焦點(diǎn)成功歸還上級(jí)作用域(當(dāng)前頁(yè)面),不過(guò)這樣如果頁(yè)面復(fù)雜,可能會(huì)比較繁瑣,你需要每個(gè)添加FocusNode來(lái)管理。所以更推薦使用:
FocusManager.instance.primaryFocus?.unfocus();
它可以直接獲取到當(dāng)前的焦點(diǎn),便于我們直接取消焦點(diǎn)。所以對(duì)比這四個(gè)方法,肯定后者比較好了,也避免了因數(shù)據(jù)錯(cuò)誤導(dǎo)致的其他隱患。
4.結(jié)語(yǔ)
通過(guò)觀察Focus Tree的變化,我們大致可以理解Focus Tree的組成及變化規(guī)律,如果你有控制焦點(diǎn)的需求,本篇或許可以為你帶來(lái)幫助。
關(guān)于Focus其實(shí)還有許多細(xì)節(jié),比如FocusAttachment如何管理FocusNode 、FocusNode的遍歷順序?qū)崿F(xiàn) FocusTraversalGroup等。由于篇幅有限,這里就不介紹了,有興趣的可以看看源碼。
本篇是“說(shuō)說(shuō)”系列第四篇,前三篇鏈接奉上:
如果本文對(duì)你有所幫助或啟發(fā)的話,還請(qǐng)不吝點(diǎn)贊收藏支持一波。同時(shí)也多多支持我的Flutter開(kāi)源項(xiàng)目flutter_deer。
我們下個(gè)月見(jiàn)~~