Focus系列的Widget及功能类在Flutter中能够说是无名英雄的存在,默默的付出但却不太为人所知。在平常开发使用中也不太会用到它,这是为何呢?带着这个问题咱们开始今天的内容。java
这里大体介绍一些Focus相关Widget及功能类,便于后面理解Focus Tree部分。本篇源码基于1.20.0-2.0.pre。node
FocusNode
是用于Widget获取键盘焦点和处理键盘事件的对象。它是继承自ChangeNotifier
,因此咱们能够在任意位置获取对应的FocusNode
信息。linux
下面说几个FocusNode
经常使用方法:android
requestFocus
用做请求焦点,注意这个请求焦点的执行放在了scheduleMicrotask
中,所以结果可能会延迟最多一帧。unfocus
用做取消焦点,默认行为为UnfocusDisposition.scope
:void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) { .... }
UnfocusDisposition
枚举类是焦点取消后的行为,分为scope
和previouslyFocusedChild
两种。git
scope
表示向上寻找最近的FocusScopeNode
。previouslyFocusedChild
是寻找上一个焦点位置,若是没有则给当前FocusScopeNode
。具体实现可见unfocus
源码,这里就很少说了。github
dispose
这个没啥说的,注意使用FocusNode
完后及时销毁。FocusScopeNode
是FocusNode
的子类。它将FocusNode
组织到一个做用域中,造成一组能够遍历的节点。它会提供最后一个获取焦点的FocusNode
(focusedChild),若是其中一个节点的焦点被移除,那么此FocusScopeNode
将再次得到焦点,同时_focusedChildren
清空。windows
/// 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
才会在里面。源码实现以下:ide
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层次结构同步。ui
咱们经常使用的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。
咱们下个月见~~