Flutter:如何在任务创建BottomSheet中实现软键盘持续显示且输入框稳定聚焦?
Flutter:如何在任务创建BottomSheet中实现软键盘持续显示且输入框稳定聚焦?
我太懂你这种需求了——就是要在任务创建的BottomSheet里,不管点日期选择、优先级设置这些交互元素,软键盘都稳稳待着,输入框也一直攥着焦点,只有关掉BottomSheet或者用系统自带的手势(比如下滑键盘)才让它消失。之前你试的那些方法总会出现讨厌的闪烁,尤其是调原生的showDatePicker时,那一下闪回真的破坏体验,我来给你一套能解决这些问题的方案。
首先要抓住两个核心:阻止BottomSheet内部的操作夺走输入框焦点,以及处理原生弹窗强制隐藏键盘后的无闪烁恢复。下面是我整理的完整实现思路和代码:
一、用FocusAttachment+焦点监听打造稳定的焦点管理
Flutter的FocusNode搭配FocusAttachment能让我们更精准地控制焦点生命周期,避免和框架自带的焦点逻辑冲突。同时监听焦点变化,一旦发现输入框失焦且BottomSheet还在显示,就自动重新请求焦点。
二、完整代码实现
class TaskBottomSheet extends StatefulWidget { const TaskBottomSheet({super.key}); @override State<TaskBottomSheet> createState() => _TaskBottomSheetState(); } class _TaskBottomSheetState extends State<TaskBottomSheet> { late final FocusNode _descriptionFocusNode; late final FocusAttachment _focusAttachment; bool _isSheetVisible = true; @override void initState() { super.initState(); _descriptionFocusNode = FocusNode(debugLabel: 'DescriptionField'); // 绑定焦点与Widget上下文,确保生命周期同步 _focusAttachment = _descriptionFocusNode.attach(context, onKey: (node, event) { return KeyEventResult.ignored; }); // 监听焦点变化,失焦时自动恢复 _descriptionFocusNode.addListener(_onFocusChange); // 初始渲染完成后请求焦点 WidgetsBinding.instance.addPostFrameCallback((_) { _descriptionFocusNode.requestFocus(); }); } void _onFocusChange() { if (!_descriptionFocusNode.hasFocus && _isSheetVisible) { // 用microtask延迟请求,避免和其他手势事件队列冲突,减少闪烁 Future.microtask(() { if (mounted && _isSheetVisible) { _descriptionFocusNode.requestFocus(); } }); } } @override void dispose() { _descriptionFocusNode.removeListener(_onFocusChange); _descriptionFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // 确保焦点附件随上下文更新 _focusAttachment.reparent(); // 适配键盘高度,避免BottomSheet被键盘遮挡 return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, left: 16, right: 16, top: 16, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( focusNode: _descriptionFocusNode, decoration: const InputDecoration( hintText: '输入任务描述...', border: OutlineInputBorder(), ), maxLines: 3, autofocus: true, // 重写点击外部的行为,阻止输入框失焦 onTapOutside: (event) { _descriptionFocusNode.requestFocus(); }, ), const SizedBox(height: 16), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () async { // 处理日期选择器,原生弹窗会强制隐藏键盘,完成后立即恢复焦点 final pickedDate = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime(2030), ); // 确保Widget还挂载时再请求焦点 if (mounted) { _descriptionFocusNode.requestFocus(); } }, icon: const Icon(Icons.calendar_today), label: const Text('选择日期'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: () { // 处理优先级选择的子BottomSheet,关闭后恢复焦点 showModalBottomSheet( context: context, builder: (ctx) => const PrioritySelector(), ).then((_) { if (mounted) { _descriptionFocusNode.requestFocus(); } }); }, icon: const Icon(Icons.flag), label: const Text('设置优先级'), ), ), ], ), const SizedBox(height: 16), ElevatedButton( onPressed: () { _isSheetVisible = false; Navigator.pop(context); }, child: const Text('保存任务'), ), ], ), ); } } // 示例优先级选择器子BottomSheet class PrioritySelector extends StatelessWidget { const PrioritySelector({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text('高优先级'), onTap: () => Navigator.pop(context, 'high'), ), ListTile( title: const Text('中优先级'), onTap: () => Navigator.pop(context, 'medium'), ), ListTile( title: const Text('低优先级'), onTap: () => Navigator.pop(context, 'low'), ), ], ), ); } }
三、关键细节解析
- FocusAttachment的作用:它能把FocusNode和Widget的上下文绑定,确保焦点管理和Widget生命周期同步,不会出现“焦点还在但Widget已销毁”的异常。
- 重写
onTapOutside:默认情况下,点击TextField外部会触发失焦,这里直接重新请求焦点,从根源上阻止内部点击导致的失焦。 - 延迟焦点请求:用
Future.microtask延迟焦点请求,是为了避开和其他手势的事件队列冲突,减少那种“闪一下又回来”的尴尬情况。 - 弹窗回调恢复焦点:像
showDatePicker这种原生弹窗,系统会强制隐藏软键盘,所以必须在它的回调里手动重新请求焦点,而且要加mounted判断,避免状态错误。 - 适配键盘高度:用
MediaQuery.of(context).viewInsets.bottom给BottomSheet底部加padding,确保输入框不会被键盘挡住,这是很容易忽略的细节。
另外要提醒你,别用SystemChannels.textInput.invokeMethod('TextInput.show')这种底层方法,它很容易和Flutter的焦点管理逻辑打架,用FocusNode的方式才是符合框架规范的正确姿势。
内容来源于stack exchange




