You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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'),
          ),
        ],
      ),
    );
  }
}

三、关键细节解析

  1. FocusAttachment的作用:它能把FocusNode和Widget的上下文绑定,确保焦点管理和Widget生命周期同步,不会出现“焦点还在但Widget已销毁”的异常。
  2. 重写onTapOutside:默认情况下,点击TextField外部会触发失焦,这里直接重新请求焦点,从根源上阻止内部点击导致的失焦。
  3. 延迟焦点请求:用Future.microtask延迟焦点请求,是为了避开和其他手势的事件队列冲突,减少那种“闪一下又回来”的尴尬情况。
  4. 弹窗回调恢复焦点:像showDatePicker这种原生弹窗,系统会强制隐藏软键盘,所以必须在它的回调里手动重新请求焦点,而且要加mounted判断,避免状态错误。
  5. 适配键盘高度:用MediaQuery.of(context).viewInsets.bottom给BottomSheet底部加padding,确保输入框不会被键盘挡住,这是很容易忽略的细节。

另外要提醒你,别用SystemChannels.textInput.invokeMethod('TextInput.show')这种底层方法,它很容易和Flutter的焦点管理逻辑打架,用FocusNode的方式才是符合框架规范的正确姿势。

内容来源于stack exchange

火山引擎 最新活动