Flutter 组件集录 | Focus 组件 - 我就是焦点~

1,025 阅读4分钟

上一篇介绍了两个关于键盘的组件 KeyboardListenerCallbackShortcuts 。通过源码的简看,引出了背后的 Focus 大佬。本文就来介绍一下 Focus 焦点组件, 你在在使用 TextFiled 组件时,可能用过 FocusNode 对象来控制输入框的激活状态,这本质上就是焦点的功能。本文中案例的代码已经收录到了FlutterUnit 中,可以在对应组件中详细查看。


1. 简单认识 Focus 组件

我们先来通过下面的简单案例,了解一下 Focus 的使用。

点击盒子可以切换 [获取/失去焦点]

如下所示,在状态类中可以创建一个 FocusNode 成员,作为 Focus 组件的构造入参,通过 onFocusChange 可以监听到焦点的变化时机。

class FocusDemo1 extends StatefulWidget {
  const FocusDemo1({super.key});

  @override
  State<FocusDemo1> createState() => _FocusDemo1State();
}

class _FocusDemo1State extends State<FocusDemo1> {
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Focus(
      focusNode: _focusNode,
      onFocusChange: _onFocusChange,
      child: GestureDetector(
        onTap: _toggleFocus,
        child: _FocusBox(active: _focusNode.hasFocus),
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();
    _focusNode.dispose();
  }

FocusNode 是焦点系统中的关键人物,它是一个 ChangeNotifier,可以被监听。其携带着是否被聚焦的数据,也可以通过方法 unfocusrequestFocus 失去焦点和获取焦点。所以该案例中,_toggleFocus 方法可以通过检查 _focusNode 是否激活,来失焦或聚焦:

  void _onFocusChange(bool value) {
    setState(() {});
  }

  void _toggleFocus() {
    if (_focusNode.hasFocus) {
      _focusNode.unfocus();
    } else {
      _focusNode.requestFocus();
    }
  }
}

2. 自动聚焦与子树访问

下面案例是一个横向的滑动列表:

  • 其中每个条目都加了 Focus 组件,蓝色背景的条目表示被聚焦的组件。
  • 默认第一项聚焦,点击的对应条目会获取焦点。
  • Flutter 框架内置了焦点切换的快捷键,比如横向滑动的列表 分别让前项和后项聚焦。Tab 键可以让下一个 Focus 节点聚焦。

下面 _FocusTiled 是条目组件,Focus 可以通过 autofocus 入参决定是否自动聚焦,TextFiled 组件的自动聚焦底层就是它的功效用。
当 Focus 构造时没有传入 FocusNode 对象,它的状态类会在内部创建一个 FocusNode。另外,Focus 可以通过 of 静态方法根据上下文查找到上层临近的 FocusNode 对象:

class _FocusTiled extends StatelessWidget {
  const _FocusTiled(
    this.data, {
    super.key,
    required this.autofocus,
  });

  final String data;
  final bool autofocus;

  @override
  Widget build(BuildContext context) {
    return Focus(
      autofocus: autofocus,
      child: Builder(builder: _buildTiled),
    );
  }

  Widget _buildTiled(BuildContext context) {
    FocusNode node = Focus.of(context);
    bool focus = node.hasFocus;
    Color color = focus ? Colors.blue : Colors.white;
    Color? textColor = focus ? Colors.white : null;
    return GestureDetector(
      onTap: node.requestFocus,
      child: Container(
        padding: const EdgeInsets.all(8.0),
        alignment: Alignment.center,
        color: color,
        child: Text( data,  style: TextStyle(color: textColor)),
      ),
    );
  }
}

从这里可以看出 Focus 本质上是一个向子树共享 FocusNode 的 InheritedWidget。这样子树中的任何组件,都可以通过上下文获取临近的 FocusNode 对象,控制焦点的行为。当聚焦之后,就可以响应键盘事件了。


3. Focus 组件监听键盘事件

上一篇,我们从源码的角度了解到 KeyboardListenerCallbackShortcuts 组件都是基于 Focus 组件的 onKeyEvent 回调函数实现的:这里详细说明一下这个回调:

它名为 FocusOnKeyEventCallback 返回 KeyEventResult 枚举,可以将 FocusNode 和 KeyEvent 事件回调出去。

typedef FocusOnKeyEventCallback = KeyEventResult Function(FocusNode node, KeyEvent event);

KeyEventResult 表示事件的处理方式:

  • handled 表示已解决,事件不应继续传播到其他键事件处理器。
  • ignored 表示忽略事件结果,事件应继续传播到其他键事件处理器,甚至包括非 Flutter 的处理器。
  • skipRemainingHandlers 表示事件键事件未被处理,但事件不应传播到其他键事件处理器。它将返回到平台嵌入层,并传播到文本字段及平台上的非 Flutter 键事件处理器。
enum KeyEventResult {
  handled,
  ignored,
  skipRemainingHandlers,
}

4. Focus 组件源码简看

Focus 组件是一个 StatefulWidget, 这表明它依赖状态类进行维护数据,以及构建内容。

从下面可以看出,默认会取 Focus 组件传入的 FocusNode,如果不传入,会创建 _internalNode 对象来使用:

FocusNode _createNode() {
  return FocusNode(
    debugLabel: widget.debugLabel,
    canRequestFocus: widget.canRequestFocus,
    descendantsAreFocusable: widget.descendantsAreFocusable,
    descendantsAreTraversable: widget.descendantsAreTraversable,
    skipTraversal: widget.skipTraversal,
  );
}

另外 State#dispose 方法中如果 _internalNode 非空会被销毁。但如果是外界传入的 FocusNode,则不会在这里主动销毁。需要使用者自行维护节点对象的生命周期。

在最最重要的 build 方法中,构建逻辑非常简单,就是将 focusNode 传递给 _FocusInheritedScope。源码中看到 XXXScope 就应该下意识地知道它是一个 InheritedWidget, 向子树传递数据:

进一步可以看出 _FocusInheritedScope 继承自 InheritedNotifier, 在 《Flutter 组件集录 | InheritedNotifier 内置状态管理组件》 一文中介绍过这个组件,感兴趣的可以去详细了解。


Focus 组件最重要的是监听键盘事件,那 onKeyEvent 是在哪里触发的呢? 在 _initNode 初始化焦点节点时,Focus 组件传入的 onKeyEvent 被传入 FocusNode#attach 方法中:

也就是说,事件处理的回调函数是由 FocusNode 类所掌握的:


尾声

这表明 Focus 组件也只是台面上 传递配置 的工具人,真正发挥作用的大佬仍在幕后。后续有机会还会更深入地挖掘 FocusNode 类的作用,以及其背后的一套焦点管理系统。敬请期待 ~

更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。