说说Flutter中的无名英雄 —— Focus

栏目: IT技术 · 发布时间: 3年前

内容简介:Focus系列的Widget及功能类在Flutter中可以说是无名英雄的存在,默默的付出但却不太为人所知。在日常开发使用中也不太会用到它,这是为什么呢?带着这个问题我们开始今天的内容。这里大致介绍一些Focus相关Widget及功能类,便于后面理解Focus Tree部分。本篇源码基于1.20.0-2.0.pre。

说说Flutter中的无名英雄 —— Focus

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 枚举类是焦点取消后的行为,分为 scopepreviouslyFocusedChild 两种。

  1. scope 表示向上寻找最近的 FocusScopeNode
  2. previouslyFocusedChild 是寻找上一个焦点位置,如果没有则给当前 FocusScopeNode

具体实现可见 unfocus 源码,这里就不多说了。

  • dispose 这个没啥说的,注意使用 FocusNode 完后及时销毁。

1.2 FocusScopeNode

FocusScopeNodeFocusNode 的子类。它将 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 源码:

说说Flutter中的无名英雄 —— Focus

这里发现了 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 ,它是表示使用何种交互模式获取的焦点。分为 touchtraditional

默认的区分实现如下:

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

FocusScopeFocus 类似,不过它的内部管理的是 FocusScopeNode 。它不改变主焦点,它只是改变了接收焦点的作用域节点。这个在源码中使用的不多,但却都很重要的位置。

比如 NavigatorRoute ,首先 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 里一个 TextFieldFlatButton

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

我从下往上说一下代表的含义:

  1. Child 1: FocusNode#e72e2Child 2: FocusNode#0b7c0 一看就是同级,代表的就是 TextFieldFlatButton
  2. 上一层 FocusScopeNode#af55c 是当前的页面,可以看到焦点目前在它上面( PRIMARY FOCUS )。它是在

MaterialPageRoute -> PageRoute -> ModalRoute -> createOverlayEntries -> _buildModalScope 方法,调用 _ModalScope 创建的。

  1. 再上一层 FocusScopeNode#4f0d5Navigator ,代码如下:
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>[],
            ),
          ),
        ),
      ),
    );
  }
  1. 再往上两层是 WidgetsAppShortcutsFocusTraversalGroup 创建的。

说说Flutter中的无名英雄 —— Focus

  1. 最顶层就是 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;
    ...
  }
  ...
}
  1. 最后是 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

可以看到当前焦点 primaryFocusFocusNode#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<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#7da34FocusNode#e72e2

  1. 使用 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#e72e2FocusNode#7da34 。不过看到这里你有没有发现一个问题。焦点已经不在 FocusScopeNode#af55c 的作用域里面了,但是 focusedChildren 里却还存在数据,如果我们这时使用如 FocusScope.of(context).focusedChild 方法,那么得到的结果就是不正确的。

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

  1. 最后一个方法就是给 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 如何管理 FocusNodeFocusNode 的遍历顺序实现 FocusTraversalGroup 等。由于篇幅有限,这里就不介绍了,有兴趣的可以看看源码。

本篇是“说说”系列第四篇,前三篇链接奉上:

如果本文对你有所帮助或启发的话,还请不吝点赞收藏支持一波。同时也多多支持我的Flutter开源项目 flutter_deer

我们下个月见~~

5.参考


以上所述就是小编给大家介绍的《说说Flutter中的无名英雄 —— Focus》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Structures and Algorithms

Data Structures and Algorithms

Alfred V. Aho、Jeffrey D. Ullman、John E. Hopcroft / Addison Wesley / 1983-1-11 / USD 74.20

The authors' treatment of data structures in Data Structures and Algorithms is unified by an informal notion of "abstract data types," allowing readers to compare different implementations of the same......一起来看看 《Data Structures and Algorithms》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具