内容简介:Focus系列的Widget及功能类在Flutter中可以说是无名英雄的存在,默默的付出但却不太为人所知。在日常开发使用中也不太会用到它,这是为什么呢?带着这个问题我们开始今天的内容。这里大致介绍一些Focus相关Widget及功能类,便于后面理解Focus Tree部分。本篇源码基于1.20.0-2.0.pre。
Focus系列的Widget及功能类在Flutter中可以说是无名英雄的存在,默默的付出但却不太为人所知。在日常开发使用中也不太会用到它,这是为什么呢?带着这个问题我们开始今天的内容。
1.Focus相关介绍
这里大致介绍一些Focus相关Widget及功能类,便于后面理解Focus Tree部分。本篇源码基于1.20.0-2.0.pre。
1.1 FocusNode
FocusNode
是用于Widget获取键盘焦点和处理键盘事件的对象。它是继承自 ChangeNotifier
,所以我们可以在任意位置获取对应的 FocusNode
信息。
下面说几个 FocusNode
常用方法:
-
requestFocus
用作请求焦点,注意这个请求焦点的执行放在了scheduleMicrotask
中,因此结果可能会延迟最多一帧。 -
unfocus
用作取消焦点,默认行为为UnfocusDisposition.scope
:
void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) { .... }
UnfocusDisposition
枚举类是焦点取消后的行为,分为 scope
和 previouslyFocusedChild
两种。
-
scope
表示向上寻找最近的FocusScopeNode
。 -
previouslyFocusedChild
是寻找上一个焦点位置,如果没有则给当前FocusScopeNode
。
具体实现可见 unfocus
源码,这里就不多说了。
-
dispose
这个没啥说的,注意使用FocusNode
完后及时销毁。
1.2 FocusScopeNode
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(); } }
1.3 Focus
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端鼠标移动到按钮上,按钮会有一个变化效果。
1.4 FocusScope
FocusScope
与 Focus
类似,不过它的内部管理的是 FocusScopeNode
。它不改变主焦点,它只是改变了接收焦点的作用域节点。这个在源码中使用的不多,但却都很重要的位置。
比如 Navigator
和 Route
,首先 Navigator
有一个 FocusScope
,自动获取焦点。在它承载的一个个路由上也会添加 FocusScope
,这样当页面跳转/Dialog弹框时可以将焦点的作用域移动到上面(通过 setFirstFocus
方法)。
类似 Drawer
也是一样。当抽屉打开时,我们的焦点作用域就要移动到 Drawer
,所以也要使用 FocusScope
。
如果我们要管理焦点,在页面中有一个 Stack
,上层覆盖了下层Widget导致下面不可操作。这时我们就可以使用 FocusScope
将焦点作用域移动至上面。
2.Focus Tree
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
的变化。
3.Focus Tree变化
现在我先点击一下输入框,在点击按钮,打印结果如下(只取最后几层):
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();
它可以直接获取到当前的焦点,便于我们直接取消焦点。所以对比这四个方法,肯定后者比较好了,也避免了因数据错误导致的其他隐患。
4.结语
通过观察Focus Tree的变化,我们大致可以理解Focus Tree的组成及变化规律,如果你有控制焦点的需求,本篇或许可以为你带来帮助。
关于Focus其实还有许多细节,比如 FocusAttachment
如何管理 FocusNode
、 FocusNode
的遍历顺序实现 FocusTraversalGroup
等。由于篇幅有限,这里就不介绍了,有兴趣的可以看看源码。
本篇是“说说”系列第四篇,前三篇链接奉上:
如果本文对你有所帮助或启发的话,还请不吝点赞收藏支持一波。同时也多多支持我的Flutter开源项目 flutter_deer 。
我们下个月见~~
5.参考
以上所述就是小编给大家介绍的《说说Flutter中的无名英雄 —— Focus》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Art of Computer Programming, Volumes 1-3 Boxed Set
Donald E. Knuth / Addison-Wesley Professional / 1998-10-15 / USD 199.99
This multivolume work is widely recognized as the definitive description of classical computer science. The first three volumes have for decades been an invaluable resource in programming theory and p......一起来看看 《The Art of Computer Programming, Volumes 1-3 Boxed Set》 这本书的介绍吧!