理解 Flutter 的键盘焦点系统
如何在你的 Flutter 应用中使用焦点系统。
本篇文档说明如何控制键盘输入的指向。若你正在实现使用物理键盘的应用(例如大多数桌面和 Web 应用),本篇文档适合你。若你的应用不会配合物理键盘使用,可以跳过本文。
概览
#Flutter 自带焦点系统,将键盘输入导向应用的特定部分。为此,用户通过点击或点按所需的 UI 元素,将输入「焦点」到应用的该部分。之后,键盘输入的文字会流向该部分,直到焦点移到应用的其他部分。焦点也可以通过按下特定键盘快捷键来移动,通常绑定到 Tab,因此有时称为「Tab 遍历」(tab traversal)。
本页探讨在 Flutter 应用中执行这些操作所用的 API,以及焦点系统的工作原理。我们注意到开发者在如何定义和使用 FocusNode
对象方面存在一些困惑。若你也有类似经历,请跳转到 创建 FocusNode 对象的最佳实践。
焦点使用场景
#以下是你可能需要了解如何使用焦点系统的一些场景示例:
术语表
#以下是 Flutter 对焦点系统各元素的术语定义。实现其中部分概念的各种类将在下文介绍。
-
焦点树 (Focus tree) - 由焦点节点构成的树,通常稀疏地映射 widget 树,代表所有可以获得焦点的 widget。
-
焦点节点 (Focus node) - 焦点树中的单个节点。该节点可以获得焦点;当它处于焦点链中时,称其「拥有焦点」。只有在拥有焦点时,它才参与处理按键事件。
-
主焦点 (Primary focus) - 焦点树中距树根最远且拥有焦点的焦点节点。按键事件正是从这个节点开始,向主焦点节点及其祖先传播。
-
焦点链 (Focus chain) - 由焦点节点组成的有序列表,从主焦点节点开始,沿焦点树的分支一直延伸到焦点树的根。
-
焦点作用域 (Focus scope) - 一种特殊的焦点节点,其职责是容纳一组其他焦点节点,并只允许这些节点获得焦点。它保存着其子树中先前哪些节点曾获得焦点的信息。
-
焦点遍历 (Focus traversal) - 以可预测的顺序从一个可获得焦点的节点移动到另一个节点的过程。这通常出现在用户按下 Tab 移到下一个可获得焦点的控件或字段时。
FocusNode 与 FocusScopeNode
#
FocusNode 和 FocusScopeNode
对象实现焦点系统的机制。它们是长生命周期对象(比 widget 更持久,类似 render object),保存焦点状态与属性,从而在 widget 树多次构建之间保持持久。它们共同构成焦点树数据结构。
它们最初旨在作为面向开发者的对象,用于控制焦点系统的某些方面,但随着时间推移,已大多演变为实现焦点系统细节。为避免破坏现有应用,它们仍保留属性的公开接口。但一般而言,它们最有用的用途是作为相对不透明的句柄,传给子 widget,以便在祖先 widget 上调用 requestFocus(),请求子 widget 获得焦点。其他属性的设置最好由 Focus
或 FocusScope
widget 管理,除非你未使用它们,或正在实现自己的版本。
创建 FocusNode 对象的最佳实践
#使用这些对象时的一些建议与禁忌包括:
-
不要在每次 build 时分配新的
FocusNode。这可能导致内存泄漏,且当节点拥有焦点时 widget 重建偶尔会导致失去焦点。 -
应在有状态 widget 中创建
FocusNode和FocusScopeNode对象。使用完毕后需要 dispose,因此应只在有状态 widget 的 state 对象内创建,以便在dispose中释放它们。 -
不要对多个 widget 使用同一个
FocusNode。否则 widget 会争夺节点属性的管理权,结果往往不符合预期。 -
应设置焦点节点 widget 的
debugLabel,以便诊断焦点问题。 -
若
FocusNode或FocusScopeNode由Focus或FocusScopewidget 管理,不要在其上设置onKeyEvent回调。若需要onKeyEvent处理器,在你想监听的 widget 子树外再包一层Focuswidget,并将该 widget 的onKeyEvent属性设为你的处理器。若你也不希望它能获得主焦点,将 widget 的canRequestFocus设为 false。这是因为Focuswidget 的onKeyEvent属性可能在后续 build 中被设为其他值,从而覆盖你在节点上设置的onKeyEvent处理器。 -
应在节点上调用
requestFocus()以请求其获得主焦点,尤其是从已将自有节点传给子代的祖先处,在你希望获得焦点的子代上调用。 -
应使用
focusNode.requestFocus()。不必调用FocusScope.of(context).requestFocus(focusNode)。focusNode.requestFocus()方法等价且性能更好。
取消焦点
#
有一个 API 用于让节点「放弃焦点」,名为 FocusNode.unfocus()。虽然它会从该节点移除焦点,但重要的是要理解,并不存在真正「取消所有节点焦点」这回事。若某节点失去焦点,焦点必须转移到别处,因为 始终 存在主焦点。节点调用 unfocus() 时接收焦点的节点,取决于传给 unfocus() 的 disposition
参数,要么是最近的 FocusScopeNode,要么是该作用域内先前拥有焦点的节点。若你想更精确地控制从某节点移除焦点后焦点去向,应显式让另一节点获得焦点,而不是调用 unfocus(),或使用焦点遍历机制,通过 FocusNode 上的 focusInDirection、nextFocus 或
previousFocus 方法找到另一节点。
调用 unfocus() 时,disposition 参数提供两种取消焦点模式:
UnfocusDisposition.scope
和 UnfocusDisposition.previouslyFocusedChild。默认为 scope,将焦点交给最近的父焦点作用域。这意味着若之后用 FocusNode.nextFocus 将焦点移到下一节点,会从作用域内「第一个」可获得焦点的项开始。
previouslyFocusedChild 处置会在作用域内查找先前拥有焦点的子节点并请求其获得焦点。若没有先前拥有焦点的子节点,则与 scope 等价。
Focus widget
#
Focus widget 拥有并管理焦点节点,是焦点系统的主力。它管理其拥有的焦点节点在焦点树上的挂载与卸载,管理焦点节点的属性与回调,并提供静态函数以便发现挂载在 widget 树上的焦点节点。
最简单用法是用 Focus widget 包裹 widget 子树,使该子树在焦点遍历过程中,或在传入的 FocusNode 上调用 requestFocus 时获得焦点。若与调用 requestFocus 的手势检测器配合,可在点按或点击时获得焦点。
你可以将 FocusNode 对象传给 Focus widget 由其管理;若不传,它会自行创建。自行创建 FocusNode 的主要原因是从父 widget 在节点上调用 requestFocus() 以控制焦点。
FocusNode 的其他大部分功能最好通过修改 Focus widget 自身的属性来访问。
Flutter 的大多数内置控件都使用 Focus widget 实现其焦点功能。
下面示例展示如何使用 Focus widget 使自定义控件可获得焦点。它创建一个带文字的容器,在获得焦点时做出反应。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Focus Sample';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[MyCustomWidget(), MyCustomWidget()],
),
),
);
}
}
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({super.key});
@override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget> {
Color _color = Colors.white;
String _label = 'Unfocused';
@override
Widget build(BuildContext context) {
return Focus(
onFocusChange: (focused) {
setState(() {
_color = focused ? Colors.black26 : Colors.white;
_label = focused ? 'Focused' : 'Unfocused';
});
},
child: Center(
child: Container(
width: 300,
height: 50,
alignment: Alignment.center,
color: _color,
child: Text(_label),
),
),
);
}
}
按键事件
#
若要在子树中监听按键事件,将 Focus widget 的 onKeyEvent 属性设为处理器,该处理器仅监听按键,或处理按键并阻止其向其他 widget 传播。
按键事件从拥有主焦点的焦点节点开始。若该节点的 onKeyEvent 处理器未返回 KeyEventResult.handled,则事件交给其父焦点节点。若父节点未处理,则继续向上,直至焦点树根。若事件到达焦点树根仍未被处理,则返回平台,交给应用中的下一个原生控件(当 Flutter UI 是更大原生应用 UI 的一部分时)。已处理的事件不会传播到其他 Flutter widget,也不会传播到原生 widget。
下面是一个 Focus widget 示例,它吸收子树未处理的每个按键,且自身不能成为主焦点:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) => KeyEventResult.handled,
canRequestFocus: false,
child: child,
);
}
焦点按键事件在文本输入事件之前处理,因此在焦点 widget 包裹文本字段时处理按键事件会阻止该键输入到文本字段。
下面是一个不允许在文本字段中输入字母「a」的 widget 示例:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
return (event.logicalKey == LogicalKeyboardKey.keyA)
? KeyEventResult.handled
: KeyEventResult.ignored;
},
child: const TextField(),
);
}
若目的是输入校验,该示例功能用 TextInputFormatter 实现可能更合适,但该技巧仍有用处:例如 Shortcuts widget 用此方法在快捷键成为文本输入之前处理它们。
控制哪些可获得焦点
#
焦点的主要方面之一是控制什么可以获得焦点以及如何获得。属性 canRequestFocus、skipTraversal 和 descendantsAreFocusable
控制该节点及其子代如何参与焦点过程。
若 skipTraversal 属性为 true,则该焦点节点不参与焦点遍历。若在其焦点节点上调用 requestFocus,仍可获得焦点,但在焦点遍历系统寻找下一个焦点目标时会被跳过。
canRequestFocus 属性(顾名思义)控制该 Focus widget 管理的焦点节点是否可用于请求焦点。若该属性为 false,在节点上调用 requestFocus 无效。这也意味着该节点在焦点遍历中被跳过,因为它无法请求焦点。
descendantsAreFocusable 属性控制该节点的子代是否可获得焦点,但仍允许该节点自身获得焦点。该属性可用于关闭整个 widget 子树的焦点能力。
ExcludeFocus widget 就是这样工作的:它只是将该属性设好的 Focus widget。
自动焦点
#
将 Focus widget 的 autofocus 属性设为 true 会告诉 widget 在其所属焦点作用域第一次获得焦点时请求焦点。若有多个 widget 设置了 autofocus,哪个获得焦点是任意的,因此尽量每个焦点作用域只在一个 widget 上设置。
仅当节点所属作用域内尚无焦点时,autofocus 属性才会生效。
在两个属于不同焦点作用域的节点上设置 autofocus 是明确定义的:各自在对应作用域获得焦点时成为获得焦点的 widget。
变化通知
#
Focus.onFocusChanged 回调可用于在特定节点的焦点状态变化时收到通知。节点被加入或移出焦点链时都会通知,这意味着即使不是主焦点也会收到通知。若你只想知道是否获得了主焦点,请检查焦点节点上的 hasPrimaryFocus 是否为 true。
获取 FocusNode
#有时需要获取 Focus widget 的焦点节点以查询其属性。
要从 Focus widget 的祖先访问焦点节点,创建 FocusNode 并作为 Focus widget 的 focusNode 属性传入。因其需要 dispose,你传入的焦点节点应由有状态 widget 拥有,不要在每次 build 时新建。
若要从 Focus widget 的子代访问焦点节点,可调用 Focus.of(context) 获取距给定 context 最近的 Focus widget 的焦点节点。若要在同一 build 函数内获取 Focus widget 的 FocusNode,请使用 Builder
确保 context 正确。如下例所示:
@override
Widget build(BuildContext context) {
return Focus(
child: Builder(
builder: (context) {
final bool hasPrimary = Focus.of(context).hasPrimaryFocus;
print('Building with primary focus: $hasPrimary');
return const SizedBox(width: 100, height: 100);
},
),
);
}
时机
#焦点系统的一个细节是:请求焦点时,仅在当前 build 阶段完成后才生效。这意味着焦点变化总是延迟一帧,因为改变焦点可能导致 widget 树任意部分(包括当前请求焦点的 widget 的祖先)重建。子代不能使祖先变脏,因此必须在帧之间进行,以便所需变化在下一帧发生。
FocusScope widget
#
FocusScope widget 是 Focus widget 的特殊版本,管理 FocusScopeNode 而非 FocusNode。
FocusScopeNode 是焦点树中的特殊节点,作为子树中焦点节点的分组机制。除非显式聚焦作用域外的节点,否则焦点遍历停留在焦点作用域内。
焦点作用域还跟踪其子树内当前焦点及曾获得焦点的节点历史。这样,若某节点在拥有焦点时释放焦点或被移除,焦点可返回到先前拥有焦点的节点。
当没有子代拥有焦点时,焦点作用域也作为焦点返回的落脚点。这使焦点遍历代码有起始上下文,用于查找下一个(或第一个)可移到的可获得焦点的控件。
若你聚焦焦点作用域节点,它会先尝试聚焦其子树中当前或最近拥有焦点的节点,或请求了 autofocus 的节点(若有)。若没有此类节点,则由作用域节点自身获得焦点。
FocusableActionDetector widget
#
FocusableActionDetector
是将
Actions、Shortcuts、MouseRegion
与 Focus widget 的功能组合在一起的 widget,用于创建定义动作与按键绑定、并提供处理焦点与悬停高亮回调的检测器。
Flutter 控件用它实现控件的上述所有方面。它仅用组成 widget 实现,因此若你不需要全部功能,可只使用需要的部分,但它是将这些行为融入自定义控件的便捷方式。
控制焦点遍历
#应用具备焦点能力后,许多应用接下来希望让用户用键盘或其他输入设备控制焦点。最常见的是「Tab 遍历」:用户按 Tab 移到「下一个」控件。控制「下一个」的含义是本节主题。 Flutter 默认提供此类遍历。
在简单网格布局中,较容易决定下一个控件。若不在行末,则是右侧(从右到左语言环境则为左侧)的控件。若在行末,则是下一行第一个控件。遗憾的是,应用很少按网格布局,因此往往需要更多指引。
Flutter 用于焦点遍历的默认算法(ReadingOrderTraversalPolicy)相当好:对大多数应用能给出正确结果。但总有极端情况,或上下文/设计要求的顺序与默认排序算法不同。对这些情况,有其他机制可实现所需顺序。
FocusTraversalGroup widget
#
FocusTraversalGroup
widget 应放在 widget 树中,包裹应在移到其他 widget 或 widget 组之前完整遍历的 widget 子树。仅将 widget 分组为相关组往往足以解决许多 Tab 遍历顺序问题。若不够,还可为组指定 FocusTraversalPolicy
以确定组内顺序。
默认的 ReadingOrderTraversalPolicy
通常足够,但若需要更多顺序控制,可使用 OrderedTraversalPolicy。包裹可获得焦点的 widget 的
FocusTraversalOrder
widget 的 order 参数决定顺序。顺序可以是 FocusOrder
的任意子类,但提供了 NumericFocusOrder
和 LexicalFocusOrder。
若提供的焦点遍历策略都不满足应用需求,你也可以编写自己的策略,以确定任意自定义顺序。
下面示例展示如何使用 FocusTraversalOrder widget,通过 NumericFocusOrder 按 TWO、ONE、THREE 顺序遍历一行按钮:
class OrderedButtonRow extends StatelessWidget {
const OrderedButtonRow({super.key});
@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: <Widget>[
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextButton(child: const Text('ONE'), onPressed: () {}),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextButton(child: const Text('TWO'), onPressed: () {}),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: TextButton(child: const Text('THREE'), onPressed: () {}),
),
const Spacer(),
],
),
);
}
}
FocusTraversalPolicy
#
FocusTraversalPolicy 是根据请求和当前焦点节点决定下一个 widget 的对象。请求(成员函数)包括 findFirstFocus、findLastFocus、next、previous
和 inDirection 等。
FocusTraversalPolicy 是具体策略的抽象基类,例如 ReadingOrderTraversalPolicy、OrderedTraversalPolicy
以及 DirectionalFocusTraversalPolicyMixin
类。
要使用 FocusTraversalPolicy,需将其交给 FocusTraversalGroup,由后者确定策略生效的 widget 子树。该类的成员函数很少直接调用:它们供焦点系统使用。
焦点管理器
#
FocusManager
维护系统当前的主焦点。它对焦点系统用户仅有少量有用 API。之一是 FocusManager.instance.primaryFocus 属性,包含当前获得焦点的焦点节点,也可通过全局 primaryFocus 字段访问。
其他有用属性包括 FocusManager.instance.highlightMode 和 FocusManager.instance.highlightStrategy。需要在其焦点高亮之间切换「触摸」模式与「传统」(鼠标和键盘)模式的 widget 会使用它们。用户用触摸导航时,焦点高亮通常隐藏;切换到鼠标或键盘时,需再次显示焦点高亮,以便知道当前焦点在哪。
highlightStrategy 告诉焦点管理器如何解释设备使用模式的变化:可根据最近输入事件自动在两种模式间切换,或锁定为触摸或传统模式。Flutter 提供的 widget 已知道如何使用这些信息,因此仅在你从零编写自己的控件时才需要。可用 addHighlightModeListener 回调监听高亮模式变化。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-17。查看文档源码 或者 为本页面内容提出建议。