上一章, 简单的使用了一下 FocusNode 和周边的一些东西, 今天来扒一扒 FocusNode 整体的附着(attach)和分离(detach)
flutter 环境还是针对 1.17.5

Focus 家族的源码分析

附着

首先第一, 平时是以 FocusNode 为主要对象的, 那么这东西是怎么附着到整体的呢

构造方法

1595382471

  • debugLabel, 这东西看名字就是 debug 用的, 先不管
  • onKey, 一看就是接受事件用的RawKeyEvent
  • canRequestFocus, 就是能不能接受焦点
  • skipTraversal, 是否接受遍历

我们再找找方法

1595381640

有几个可能用到的关键字段, 我们一个个看, 但是不一定会用的到, 但是有所了解也是好

  • context, 这个是和控件结合 的, 单纯看这里还看不出来

  • _manager, 我们知道这个在 flutter 环境中其实是全局单例的, 但是这里为了解耦所以可能是哪里传进来的, 我们理论上可以忽略

  • _ancestors, 嗯.. 没有注释, 那就只能看看源码了 1595381928 所以, 这个东西是把所有的父节点都装到了这个里. 嗯, 个人经验,应用层的话大概率用不上.

  • _descendants,这东西也是个三无, 没事 1595382072 都在源码里了, 这东西是深度为 1 的子 nodedescendants 和子node, 然后里面递归了… 换句话说, 其实, 这东西是所有的子 node

  • _hasKeyboardToken 这东西暂时没看出作用, 先放放

  • children, 这东西就是所有子node, 怎么附着的暂时还不知道, 先放放

  • traversalChildren, 这就是上次用到的那东西的核心了, 就是靠这东西筛选了一下, 可以接受遍历的子节点, 这里就和构造方法那对上了 1595382613

找了一些核心字段, 但是还是没找到怎么附着的, 我们整体看看方法名 1595382682 好家伙, 一屏都要装不下了, 看看名字, 直奔 attach 1595382729

这方法看起来就是附着的方法了, 咱们跟一下这个方法, 注意到其实内部只有绑定 context, 绑定 key 回调, 然后创建了一个 attachment, 这东西是何方神圣, 咱们来跟进去看看

1595396089

这东西看介绍是用来保证 FocusNode 不会被多处附着的, 怎么实现的呢

1595396295 1595396309 好吧, 看上去就是检测一下自己位置对不对, 不对就重新找个父节点

Node 的树形结构

上面简单的看了一下源码的结构, 但是可能还是不能系统的想象到底有哪些 node

我写一个方法简单输出一下 FocusNode 的节点关系

import 'package:flutter/material.dart';

class FocusNodePrintExample extends StatefulWidget {
  @override
  _FocusNodePrintExampleState createState() => _FocusNodePrintExampleState();
}

class _FocusNodePrintExampleState extends State<FocusNodePrintExample> {
  FocusNode node = FocusNode(debugLabel: 'the input');
  FocusNode node2 = FocusNode(debugLabel: 'the raised button');
  FocusNode node3 = FocusNode(debugLabel: 'the float button');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          TextField(
            focusNode: node,
          ),
          RaisedButton(
            onPressed: () {},
            focusNode: node2,
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        focusNode: node3,
        onPressed: () {
          priceAllFocusNode();
        },
      ),
    );
  }
}

void printNode(int level, FocusNode node) {
  print('${'--' * level} $node');
  for (final child in node.children) {
    printNode(level + 1, child);
  }
}

void priceAllFocusNode() {
  printNode(0, FocusManager.instance.rootScope);
}

1595405011

这样就比较清晰了, 一个页面, 即使在没有自己使用 FocusScope 组织的情况下也会有自己的 FocusNode, 然后所有子节点会"自发"的形成一个并列的 FocusNode 关系, 并附着到当前页的 FocusNode 上, 这个 ModalScopeState就是让每一个页面有自己的焦点区域, 从而不会互相影响

image-20200723090715817

image-20200723090725399

这两处就是这东西定义FocusScope的地方, 然后这东西本身的定义是在ModalRoute里面

image-20200723090958113

ModalRoute是各种常见的PageRoute(MaterialPageRoute,CupertinoPageRoute的父类),PopupRoute(DialogRoute的父类), 嗯,路由和页面的洗也串联起来了. 同时, 我们也知道为什么单独的 dialog 也好,页面也好, 焦点都是独立的

改变焦点

上面把焦点的区域等等东西探索完毕, 接下来看看改变焦点的时候都做了什么, requestFocus这个方法在整个 flutter 中只定义了一次

image-20200723092332981

可以看到, 这东西可以接受一个入参, 是另一个 node, 但对于这 node 有所限制, 比如属于当前 node 的父节点. 当然不传的话, 就是调用_doRequestFocus的事了, 我们再来看看这东西了做了什么

image-20200723092553734

根据注释, 这东西在scopeNode里会被覆盖, 我们一会儿看覆盖的, 先看当前的, 比如如果焦点本身canRequestFocus是 false, 就返回了(debug 会报错我知道, 但是咱们以 release 运行结果为准).

然后如果父节点是空, 会设置一个_requestFocusWhenReparented为 true, 看名字应该是当重新附着的时候请求焦点, 换句话说先记账,我个人对于这里的理解是, 如果你开了新页面, 因为整个旧页面都失去了焦点, 但是当这个旧页面返回前台时, 检测这个东西是 true 时, 就会重新请求焦点.

image-20200723093049253

把当前父焦点按顺序添加到末尾的意思, 具体作用不明

然后是

if (hasPrimaryFocus && (_manager._markedForFocus == null || _manager._markedForFocus == this)) {
  return;
}

这里的作用就是如果有有主焦点, 且已经被标记为当前焦点, 才 return, hasPrimaryFocus是表示就是当前主焦点. _markedForFocus表示虽然申请了, 但是暂时还没活动, 类似于一个脏标记, 这个变量应该是给下一帧使用的.

_hasKeyboardToken = true;
assert(_focusDebug('Node requesting focus: $this'));
_markNextFocus(this);

这东西理解起来就比较简单了, 获取键盘 token, 标记下一个焦点是自己


然后, 来看看FocusScopeNode里这个方法的实现

其实就是安排 manager 来做标记

manager 的处理过程

刚刚有一个方法是_markNextFocus, 这个方法会调用到 manager 里, 我们来跟一下这个方法, 看看 manager 这个单例管理类里都干了什么image-20200723113143595

image-20200723113231263

如果是当前的主焦点, 就不管

如果不是, 则调用 update 方法

image-20200723113400797

就是一个标记方法, 然后让程序在回调里执行 focusChange

2020-07-23 at 11.35 AM

分离(detach)

image-20200723133725482

核心大概就是这些, 在FocusAttachment内,如果已经附着, 则unfocus一下, 然后到 manager 里标记分离, 解除父类和子类的关系, 接着置空_node._attachment. 可以理解为生命周期, 你在绑定做了什么, 到了解绑的时候自然要反向操作

软键盘弹起的问题

有的同学看完了焦点相关的东西会感觉奇怪了, 怎么没有软键盘的东西, 我们讲了半天焦点, 没看见有软键盘弹起的逻辑啊?

其实这是因为前面的部分只是焦点部分, 我在前文中提及过, 在 flutter 中, 不止是文本输入框才能获取焦点, 而是任何控件(比如按钮)都存在理论上获得焦点的可能性.

反向查找

如何反向查找呢? 我通过知识的积累(其实就是各种瞎看)知道了所有 flutter 和原生交互都是通过SystemChannels来进行的, 那我们来一路跟一跟

image-20200723114525795

挖, 就是这个, 就是这个, 但是软键盘是通过 textInput 这东西过来的, 我们看看这东西是怎么实现的

TextInput

image-20200723114716302

好吧, 就是一个 channel, 但是这东西有什么方法呢, 我们看看调用和封装吧.

image-20200723114837003

有这么一个类专门封装了这东西, 正向调用和回调

image-20200723114920368

看名字就是编辑状态, action(软键盘的回车之类的?), 更新光标, 关闭. 这里先不细看

updateEditingState

image-20200723115211843

image-20200723115122526

image-20200723115328513

这就和平时的onChanged回调对应上了

performAction

image-20200723115510148

看名字基本就知道了, 对应的是各种键盘的回车键功能

updateFloatingCursor

image-20200723115621549

基本就是因为点击之类或者别的什么原因, 光标位置要发生变化

connectionClosed

image-20200723115726998

这个就没啥可说的了, 断开与软键盘连接的回调

show

image-20200723120022040

image-20200723120054204

当控件有焦点时,, 就可以调用 TextInput 的show方法了, 这也就是软键盘弹出的逻辑了.

这时候又有同学要问了, 道理我都懂, 那为啥会触发到这个方法呢? 我说你这人怎么这么讨厌…

好吧, 从源码的角度来看, 也是有出处的

image-20200723133152880

image-20200723133207157

image-20200723133220009

经过这素质三连, 我觉得真相大白了, 本身focusNode是一个ChangeNotifier, 当这东西发生变化时, 自然会通知到回调方法(_handleFocusChanged), 然后我们知道之前的 manager 那边有方法会调用到这个 notify, 所以键盘自然就会弹起来了.

后记

本篇主要是解析源码中的焦点和软键盘弹起的相关部分, 本篇系列文章也许还有第三篇, 也许没有, 一切随缘

以上.