问题背景
结合业务需求,封装实现了一个自带下拉框并支持文本搜索的小组件,下拉框通过点击文本输入框进行视图触发,并且下拉数据要支持导航栏上下拖动;整体逻辑并不复杂,但是发现触发下拉框后,文本输入框输入文本不被响应,进行排查猜测大致是焦点被下拉弹窗视图竞争导致;对场景进行了总结分析进行相关知识点记录;
核心逻辑
dart
// TextField 部分
Widget buildTextField() {
return MouseRegion(
child: TextField(
controller: controller,
onTap: () => _onShowElements(), // 点击触发弹窗显示
onChanged: (val) => {
//输入框逻辑
}
},
),
);
}
// 点击事件处理
void _onShowElements() {
//计算下拉窗位置
renderBox = anchorKey.currentContext.findRenderObject() as RenderBox;
if (renderBox is null) return;
width = renderBox.size.width;
// 调用 Navigator 展示下拉选项弹窗
popWidget = Navigator.push(context, ElementWidget(child: DataList()));;
}
调用链
问题分析
焦点拦截: Navigator.push PopWidget 后,由于弹出层未正确管理焦点作用域,导致输入框的FocusNode被强制释放
解决策略
对于焦点竞争可以通过以下三种方式解决:
- 调整 MenuContainer 的交互行为(使用 IgnorePointer 或仅拦截必要区域);
dart
OverlayEntry(
builder: (context) {
return Positioned(
left: left,
top: top,
width: width,
child: IgnorePointer(
ignoring: true, // 或者只在非交互区域设置为 true
child: MenuContainer(...),
),
);
},
);
- 自动关闭弹出层以释放遮挡,从而确保用户输入时不会受到阻碍
dart
void _onShowElements({String changeValue = ""}) {
// 现有逻辑...
popWidget = _showOptions(...);
// 使 TextField 保持焦点
FocusScope.of(context).requestFocus(_focusNode);
}
- 强制管理焦点,确保 TextField 始终拥有输入焦点;
dart
void _onShowElements({String changeValue = ""}) {
// 现有逻辑...
popWidget = _showOptions(...);
// 使 TextField 保持焦点
FocusScope.of(context).requestFocus(_focusNode);
}
鉴于当前弹窗也需要通过焦点进行功能处理,并需要满足使用输入框时焦点控制在输入框内且下拉窗视图不被关闭,因此将通过管理焦点方式尝试解决
场景拓展
场景1:多层弹窗叠加时的焦点穿透
- 现象:多个弹窗(如确认弹窗+输入弹窗)叠加时,底层弹窗可能意外获取焦点。
- 示例:用户先打开日期选择弹窗,再触发一个错误提示弹窗,此时日期选择器的焦点被错误弹窗覆盖。
场景2:动态组件中的焦点跳跃
- 现象:列表项动态生成可编辑控件时,焦点切换逻辑混乱。
- 示例:在可编辑列表中,新增行导致已有行的输入框焦点丢失。
场景3:键盘交互与布局挤压
- 现象:移动端键盘弹出挤压页面布局,焦点控件被遮挡。
- 示例:底部输入框获得焦点时,键盘升起遮挡输入区域。
场景4:无障碍访问焦点混乱
- 现象:屏幕阅读器无法正确识别动态焦点变化。
- 示例:视障用户使用TalkBack时,弹窗出现后阅读焦点仍停留在原页面。
通用解决策略
问题类型 | 核心思路 | Flutter实现方案 |
---|---|---|
焦点竞争 | 精确控制焦点作用域 | 使用FocusScope 隔离不同区域焦点,通过FocusNode.requestFocus() 主动管理 |
弹窗叠加 | 分层管理Overlay层级 | 采用OverlayEntry + Stack 手动控制弹窗层级,而非默认Navigator 堆栈 |
动态组件焦点跟踪 | 建立组件与焦点的动态绑定 | 为动态组件分配唯一GlobalKey ,通过Focus.of(context,scope: customScope) 精准定位 |
键盘挤压布局 | 动态适配视图布局 | 结合MediaQuery.of(context).viewInsets 计算安全区域,使用SingleChildScrollView 适配 |
无障碍访问兼容 | 遵循WCAG焦点管理规范 | 使用Semantics 组件声明焦点顺序,配合FocusTraversalGroup 管理阅读顺序 |
源码解析
Flutter 的焦点系统基于 树形结构 ,通过 FocusManager
管理全局焦点状态,每个 FocusNode
或 FocusScopeNode
作为树中的节点
dart
FocusManager
├── FocusScopeNode(root)
│ ├── FocusScopeNode(page1)
│ │ ├── FocusNode(textField1)
│ │ └── FocusNode(button1)
│ └── FocusScopeNode(dialog)
│ └── FocusNode(textField2)
└── FocusAttachment // 管理节点在树中的位置
- FocusManager:全局单例,管理整个焦点树的状态。
- FocusScopeNode:焦点作用域节点,隔离不同区域的焦点(如页面、弹窗)。
- FocusNode :可聚焦组件的焦点控制器(如
TextField
、Button
)。 - FocusAttachment :负责将
FocusNode
挂载到焦点树中。
1. 焦点请求(Focus Request)
当组件请求焦点时,FocusNode.requestFocus()
触发以下流程:
dart
// FocusNode 类
void requestFocus([FocusOnKeyCallback? onKey]) {
FocusManager.instance.setCurrentFocus(
this,
onKey: onKey,
alignmentPolicy: _alignmentPolicy,
);
}
// FocusManager 类
void setCurrentFocus(FocusNode node, {FocusOnKeyCallback? onKey}) {
if (_currentFocus != node) {
_currentFocus?._unfocus(); // 释放原焦点
node._focus(); // 设置新焦点
_currentFocus = node;
}
}
2. 焦点作用域(FocusScope)
dart
// FocusScopeNode 类
void setFirstFocus(FocusNode node) {
if (!_children.contains(node)) return;
_focusedChild = node; // 设置当前作用域的活跃焦点
}
// 弹窗场景下,新作用域自动激活
void _activate() {
FocusManager.instance.pushScope(this); // 压入作用域栈
}
3. 焦点事件传递
dart
// RawKeyboard 事件处理
void _handleKeyEvent(RawKeyEvent event) {
if (_currentFocus != null) {
_currentFocus!.onKey(event); // 将键盘事件传递至当前焦点节点
}
}
防御性编程
1. 作用域隔离
dart
// 创建独立作用域防止焦点泄漏
FocusScopeNode _dialogScope = FocusScopeNode();
void showDialog() {
FocusManager.instance.pushScope(_dialogScope);
}
void closeDialog() {
FocusManager.instance.removeScope(_dialogScope);
}
2. 焦点防抖
dart
// 避免频繁焦点切换导致性能问题
Timer? _debounceTimer;
void safeRequestFocus(FocusNode node) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 100), () {
node.requestFocus();
});
}
焦点跟踪实现
- 用户交互触发
用户点击 TextField
,触发其关联的 FocusNode
的 requestFocus()
方法。
- 焦点请求提交
FocusNode
通过 FocusManager.instance.setCurrentFocus(this)
向全局管理器提交请求。
dart
void requestFocus() {
FocusManager.instance.setCurrentFocus(this); // 向 FocusManager 提交请求
}
- 旧焦点释放
FocusManager
调用旧焦点的 _unfocus()
方法,触发 onFocusChange
监听器。
- 新焦点设置
新 FocusNode
调用 _focus()
方法,更新内部状态并触发 onFocusChange
。
dart
void setCurrentFocus(FocusNode node) {
if (_currentFocus != node) {
_currentFocus?._unfocus(); // 释放旧焦点
node._focus(); // 设置新焦点
_currentFocus = node; // 更新当前焦点
}
}
- 作用域切换
若新焦点属于新的 FocusScopeNode
(如弹窗),则调用 pushScope()
将其压入作用域栈顶。
dart
void activate() {
FocusManager.instance.pushScope(this); // 压入作用域栈顶
_focusedChild?.requestFocus(); // 激活子节点焦点
}
- UI 状态更新
焦点变更触发 build()
方法重绘,显示光标闪烁或高亮边框。
- 事件持续监听
输入过程中,键盘事件通过 RawKeyboard
传递至当前焦点节点处理。
dart
void handleKeyEvent(RawKeyEvent event) {
if (_currentFocus != null) {
_currentFocus!.onKey(event); // 将键盘事件传递给当前焦点组件
}
}