上一篇介绍了两个关于键盘的组件 KeyboardListener 和 CallbackShortcuts 。通过源码的简看,引出了背后的 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,可以被监听。其携带着是否被聚焦的数据,也可以通过方法 unfocus
和 requestFocus
失去焦点和获取焦点。所以该案例中,_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 组件监听键盘事件
上一篇,我们从源码的角度了解到 KeyboardListener 和 CallbackShortcuts 组件都是基于 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 站 。