热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Flutter中的无名英雄——Focus

这里大致介绍一些Focus相关Widget及功能类,便于后面理解FocusTree部分。本篇源码基于1.20.

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两种。

  1. scope表示向上寻找最近的FocusScopeNode。
  2. 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 _focusedChildren = [];

注意这里的_focusedChildren并不是FocusScopeNode下出现的所有FocusNode,而是获取过焦点的FocusNode才会在里面。源码实现如下:

  void _setAsFocusedChildForScope() {     FocusNode scopeFocus = this;     for (final FocusScopeNode ancestor in ancestors.whereType()) {       // 从聚焦的历史中移除       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 { @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 │ Focus Scope [PRIMARY FOCUS]) │ nextFocus: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS PATH]) │ primaryFocusCreator: FocusScope ← _ActionsMarker ← Actions ← │ PageStorage ← Offstage ← _ModalScopeStatus ← │ _ModalScope-[LabeledGlobalKey<_ModalScopeState>#bfb70] │ ← _EffectiveTickerMode ← TickerMode ← │ _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#3fa85] │ ← _Theatre ← Overlay-[LabeledGlobalKey#2d724] ← │ _FocusMarker ← Semantics ← FocusScope ← AbsorbPointer ← │ _PointerListener ← Listener ← HeroControllerScope ← │ Navigator-[GlobalObjectKey │ _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 Focus Scope [PRIMARY FOCUS]) │ context: FocusScope │ PRIMARY FOCUS │ ├─Child 1: FocusNode#e72e2 │ context: EditableText-[LabeledGlobalKey#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 [],             ),           ),         ),       ),     );   }


再往上两层是WidgetsApp的Shortcuts和FocusTraversalGroup创建的。

WidgetsApp源码

 最顶层就是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 Focus Scope [IN FOCUS PATH]) │ context: FocusScope │ IN FOCUS PATH │ focusedChildren: FocusNode#e72e2([PRIMARY FOCUS]) │ ├─Child 1: FocusNode#e72e2([PRIMARY FOCUS]) │ context: EditableText-[LabeledGlobalKey#c2f8a] │ PRIMARY FOCUS │ └─Child 2: FocusNode#0b7c0 context: Focus

可以看到当前焦点primaryFocus为FocusNode#e72e2也就是到了TextField上。注意这里的focusedChildren此时只有FocusNode#e72e2。

因为我点击了TextField,此时软键盘弹出。现在我需要关闭软键盘,我这里有四种方法:

1. 使用SystemChannels.textInput.invokeMethod('TextInput.hide')方法,这种方法关闭软键盘后焦点不变,还在TextField上,所以有一个问题。比如这时你push到一个新的页面再pop返回,此时软键盘会再次弹出。这里不推荐使用。

2. 使用FocusScope.of(context).requestFocus(FocusNode())方法,并打印一下Focus Tree。

primaryFocus: FocusNode#7da34([PRIMARY FOCUS]) └─Child 1: FocusScopeNode#af55c(_ModalScopeState Focus Scope [IN FOCUS PATH]) │ context: FocusScope │ IN FOCUS PATH │ focusedChildren: FocusNode#7da34([PRIMARY FOCUS]), │ FocusNode#e72e2 │ ├─Child 1: FocusNode#e72e2 │ context: EditableText-[LabeledGlobalKey#c2f8a] │ ├─Child 2: FocusNode#0b7c0 │ context: Focus └─Child 3: FocusNode#7da34([PRIMARY FOCUS]) PRIMARY FOCUS

可以看到其实就在当前节点下创建了一个FocusNode#7da34并把焦点转移给它。注意这里的focusedChildren此时有FocusNode#7da34和FocusNode#e72e2。

3. 使用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 Focus Scope)     │ context: FocusScope     │ focusedChildren: FocusNode#e72e2, FocusNode#7da34     │     ├─Child 1: FocusNode#e72e2     │   context: EditableText-[LabeledGlobalKey#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方法,那么得到的结果就是不正确的。

稳妥的做法是使用下面的第四种方法。

4. 最后一个方法就是给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等。由于篇幅有限,这里就不介绍了,有兴趣的可以看看源码。
 


推荐阅读
  • 开发笔记:深入解析Android自定义控件——Button的72种变形技巧
    开发笔记:深入解析Android自定义控件——Button的72种变形技巧 ... [详细]
  • 深入解析 Android 中 EditText 的 getLayoutParams 方法及其代码应用实例 ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 优化后的摘要:默认情况下,PopupWindow在点击外部区域时会自动关闭。为了实现点击外部区域时不自动关闭的功能,可以通过自定义设置来调整PopupWindow的行为,确保其在外部点击时仍保持显示状态。这需要对PopupWindow的属性进行适当的修改和配置,以满足特定的交互需求。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 在Android开发中,实现多点触控功能需要使用`OnTouchListener`监听器来捕获触摸事件,并在`onTouch`方法中进行详细的事件处理。为了优化多点触控的交互体验,开发者可以通过识别不同的触摸手势(如缩放、旋转等)并进行相应的逻辑处理。此外,还可以结合`MotionEvent`类提供的方法,如`getPointerCount()`和`getPointerId()`,来精确控制每个触点的行为,从而提升用户操作的流畅性和响应性。 ... [详细]
  • 掌握Android UI设计:利用ZoomControls实现图片缩放功能
    本文介绍了如何在Android应用中通过使用ZoomControls组件来实现图片的缩放功能。ZoomControls提供了一种简单且直观的方式,让用户可以通过点击放大和缩小按钮来调整图片的显示大小。文章详细讲解了ZoomControls的基本用法、布局设置以及与ImageView的结合使用方法,适合初学者快速掌握Android UI设计中的这一重要功能。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • oracle c3p0 dword 60,web_day10 dbcp c3p0 dbutils
    createdatabasemydbcharactersetutf8;alertdatabasemydbcharactersetutf8;1.自定义连接池为了不去经常创建连接和释放 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 本文详细介绍了一种利用 ESP8266 01S 模块构建 Web 服务器的成功实践方案。通过具体的代码示例和详细的步骤说明,帮助读者快速掌握该模块的使用方法。在疫情期间,作者重新审视并研究了这一未被充分利用的模块,最终成功实现了 Web 服务器的功能。本文不仅提供了完整的代码实现,还涵盖了调试过程中遇到的常见问题及其解决方法,为初学者提供了宝贵的参考。 ... [详细]
  • QT框架中事件循环机制及事件分发类详解
    在QT框架中,QCoreApplication类作为事件循环的核心组件,为应用程序提供了基础的事件处理机制。该类继承自QObject,负责管理和调度各种事件,确保程序能够响应用户操作和其他系统事件。通过事件循环,QCoreApplication实现了高效的事件分发和处理,使得应用程序能够保持流畅的运行状态。此外,QCoreApplication还提供了多种方法和信号槽机制,方便开发者进行事件的定制和扩展。 ... [详细]
  • 手指触控|Android电容屏幕驱动调试指南
    手指触控|Android电容屏幕驱动调试指南 ... [详细]
  • 在 Windows 10 环境中,通过配置 Visual Studio Code (VSCode) 实现基于 Windows Subsystem for Linux (WSL) 的 C++ 开发,并启用智能代码提示功能。具体步骤包括安装 VSCode 及其相关插件,如 CCIntelliSense、TabNine 和 BracketPairColorizer,确保在 WSL 中顺利进行开发工作。此外,还详细介绍了如何在 Windows 10 中启用和配置 WSL,以实现无缝的跨平台开发体验。 ... [详细]
author-avatar
故事与酒
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有