avatar
Published on

Flutter 监听软键盘弹出 / 关闭:官方原生方案(替代 keyboard_visibility)

Flutter 监听软键盘弹出 / 关闭:官方原生方案(替代 keyboard_visibility)

Flutter 监听软键盘弹出/关闭:官方原生方案(替代 keyboard_visibility)

在 Flutter 开发中,监听软键盘的弹出与关闭是高频需求,但第三方插件 keyboard_visibility 已停止维护,存在编译报错(如 build.gradle 冲突)等问题,不推荐在新项目中使用。

推荐使用 Flutter 官方提供的 WidgetsBindingObserver 原生方案,无需依赖第三方库,稳定性更高、兼容性更强。以下是完整的实现步骤和注意事项。

核心方案:使用 WidgetsBindingObserver 监听

通过继承 WidgetsBindingObserver 并监听设备尺寸变化,间接判断软键盘的显示状态(软键盘弹出/关闭会改变屏幕可用区域)。

完整实现代码

import 'package:flutter/material.dart';

class KeyboardListenPage extends StatefulWidget {
  const KeyboardListenPage({super.key});

  
  State<KeyboardListenPage> createState() => _KeyboardListenPageState();
}

class _KeyboardListenPageState extends State<KeyboardListenPage> 
    with WidgetsBindingObserver { // 继承监听类

  // 记录键盘是否显示
  bool _isKeyboardVisible = false;

  
  void initState() {
    super.initState();
    // 注册监听器
    WidgetsBinding.instance.addObserver(this);
  }

  /// 监听设备尺寸变化(软键盘弹出/关闭会触发)
  
  void didChangeMetrics() {
    super.didChangeMetrics();
    // 延迟执行,确保获取到最新的视图信息
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) { // 防止页面销毁后 setState
        setState(() {
          // 通过 viewInsets.bottom 判断键盘状态:>0 表示弹出,=0 表示关闭
          _isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
        });
        
        // 执行自定义业务逻辑
        if (_isKeyboardVisible) {
          print("✅ 软键盘弹出");
          // 键盘弹出时的操作:如滚动列表、隐藏底部组件等
        } else {
          print("❌ 软键盘关闭");
          // 键盘关闭时的操作:如恢复页面状态、保存输入内容等
        }
      }
    });
  }

  
  void dispose() {
    // 移除监听器,避免内存泄漏
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      // 关键配置:必须设为 false,否则 viewInsets.bottom 始终为 0
      resizeToAvoidBottomInset: false,
      appBar: AppBar(title: const Text("键盘监听示例")),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            const TextField(
              decoration: InputDecoration(
                hintText: "输入内容触发键盘",
                border: OutlineInputBorder()
              ),
            ),
            const SizedBox(height: 20),
            Text(_isKeyboardVisible ? "键盘已弹出" : "键盘已关闭"),
          ],
        ),
      ),
    );
  }
}

关键注意事项(必看)

1. 解决 viewInsets.bottom 始终为 0 的问题

核心原因:父组件 Scaffold 的 resizeToAvoidBottomInset 属性默认值为 true,会自动调整页面尺寸以避免输入框被键盘遮挡,导致无法获取键盘高度。

解决方案:将所有父级 Scaffold 的 resizeToAvoidBottomInset 统一设置为 false(包括嵌套页面的 Scaffold)。

2. 避免内存泄漏

  • 初始化时通过 addObserver 注册监听器,必须在 dispose 中通过 removeObserver 移除。
  • 执行 setState 前需判断 mounted,防止页面销毁后仍触发状态更新。

3. 延迟执行的必要性

通过 addPostFrameCallback 延迟获取 viewInsets.bottom,确保在 Widget 构建完成后获取最新的视图数据,避免数据不准确。

拓展场景:获取键盘具体高度

若需要获取键盘的实际高度(而非仅判断显示状态),可通过计算屏幕尺寸变化实现:

double _keyboardHeight = 0;
// 记录初始屏幕高度
double _initialScreenHeight = 0;


void initState() {
  super.initState();
  WidgetsBinding.instance.addObserver(this);
  // 初始化时获取屏幕高度
  _initialScreenHeight = MediaQuery.of(context).size.height;
}


void didChangeMetrics() {
  super.didChangeMetrics();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) {
      final currentScreenHeight = MediaQuery.of(context).size.height;
      final viewInsetsBottom = MediaQuery.of(context).viewInsets.bottom;
      
      // 键盘高度 = 初始屏幕高度 - 当前屏幕高度(仅键盘弹出时有效)
      _keyboardHeight = viewInsetsBottom > 0 
          ? _initialScreenHeight - currentScreenHeight 
          : 0;
      
      print("键盘高度:$_keyboardHeight");
    }
  });
}

方案优势

  1. 原生支持:基于 Flutter 官方 API,无需第三方依赖,兼容性覆盖所有 Flutter 版本。
  2. 稳定性高:无编译报错、版本冲突等问题,维护成本低。
  3. 功能灵活:可仅判断状态,也可获取具体高度,适配多种业务场景。

参考资料