Flutter App 中弹窗与软键盘互动的几种方式

Flutter的弹窗与软键盘交互

前言

开发过 Flutter 应用的同学可能对软键盘多多少少都被坑过,但是大多数使用场景是页面中的软键盘弹起之后遮挡布局的问题,其实也好解决。

主流的方案是加滚动布局,或者设置 Scaffold 的 resizeToAvoidBottomInset 属性,但是如果是弹窗呢?如果不做处理是什么情况?

上图:

如果做了适配之后应该是这样的:

那么弹窗与软键盘的适配怎么做呢?

一、软键盘开关监听

Flutter 中有一些软键盘的插件,可以监听和获取到不同平台下软键盘的弹出与收起的状态,我们可以通过监听软键盘的弹出与收起,给 Dialog 的布局设置不同的 Padding 即可。

比如以我用的 flutter_keyboard_visibility 插件为例:

scala 复制代码
class VerifyPaymentPasswordView extends StatefulWidget {
  VerifyPaymentPasswordView({super.key, this.confirmAction});

  VoidCallback? confirmAction;

  @override
  State<VerifyPaymentPasswordView> createState() => _VerifyPaymentPasswordViewState();
}

class _VerifyPaymentPasswordViewState extends State<VerifyPaymentPasswordView> {
  final TextEditingController _controller = TextEditingController();
  final FocusNode _node = FocusNode();
  bool isKeyboardVisible = false;
  StreamSubscription<bool>? subscription;

  void _updateKeyboardVisibility(bool isVisible) {
    setState(() {
      isKeyboardVisible = isVisible;
    });
  }

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _node.requestFocus();
    });

    var keyboardVisibilityController = KeyboardVisibilityController();
    subscription = keyboardVisibilityController.onChange.listen((bool visible) {
      _updateKeyboardVisibility(visible);
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: isKeyboardVisible?MediaQuery.of(context).viewInsets:EdgeInsets.zero,

      ...
    );
  }

效果:

如果是底部的弹窗呢?

scss 复制代码
class ReviewDialog extends StatefulWidget {
  const ReviewDialog({Key? key}) : super(key: key);

  @override
  State<ReviewDialog> createState() => _ReviewDialogState();
}

class _ReviewDialogState extends State<ReviewDialog> {
  final _focusNode = FocusNode();
  bool isKeyboardVisible = false;
  StreamSubscription<bool>? subscription;
    
  void _updateKeyboardVisibility(bool isVisible) {
    setState(() {
      isKeyboardVisible = isVisible;
    });
  }

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _focusNode.requestFocus();
    });

    var keyboardVisibilityController = KeyboardVisibilityController();
   subscription = keyboardVisibilityController.onChange.listen((bool visible) {
      Log.d('Keyboard visibility update. Is visible: $visible');
      _updateKeyboardVisibility(visible);
    });

    super.initState();
  }
    
  @override
  void dispose() {
    subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        bottom: isKeyboardVisible ? MediaQuery.of(context).viewInsets.bottom : 0,
      ),
      child: Container(
        height: 100,
        width: double.infinity,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(10),
        ),
        alignment: Alignment.center,
        child: TextField(focusNode: _focusNode),
      ),
    );
  }
}

只设置底部的 padding 即可,只是具体弹出Dialog的时候,对齐方式的不同而已,具体的交互是一样的效果。

看代码或效果图(GIF掉帧不明显)能看出,这种效果是可以完成我们要的效果了,但是突然的上下跳动相对来说比较突兀。

二、监听 EdgeInsets 值变化

能不能监听软键盘弹起的过程中整个页面中 EdgeInsets 的变化呢?让我们的布局跟随这个状态不停的刷新不就可以实现类似动画的效果了吗?

如何监听页面中 EdgeInsets 的变化呢?

scala 复制代码
class ReviewDialog extends StatefulWidget {
  const ReviewDialog({Key? key}) : super(key: key);

  @override
  State<ReviewDialog> createState() => _ReviewDialogState();
}

class _ReviewDialogState extends State<ReviewDialog> with WidgetsBindingObserver {
  final _focusNode = FocusNode();
  EdgeInsets viewInsets = EdgeInsets.zero;

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _focusNode.requestFocus();
    });

    // 注册 WidgetsBindingObserver
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    // 移除 WidgetsBindingObserver
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    // 获取最新的 viewInsets 值
    final newInsets = WidgetsBinding.instance.window.viewInsets;
    setState(() {
      viewInsets = EdgeInsets.fromViewPadding(newInsets, WidgetsBinding.instance.window.devicePixelRatio);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        bottom:  viewInsets.bottom, 
      ),
      child: Container(
        height: 100,
        width: double.infinity,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(10),
        ),
        alignment: Alignment.center,
        child: TextField(focusNode: _focusNode),
      ),
    );
  }
}

这里就不重复贴居中的弹窗布局了,是一样的监听方案。

居中弹窗效果:

底部弹窗效果:

效果比软键盘弹起与收起的监听的效果要稍好。

三、直接用AnimatedPadding或Padding

为什么要监听,浪费内存与性能。直接使用 AnimatedPadding 设置一个目标的 Padding 值与动画执行时间,让弹窗按自己的方式做动画,这是网上推荐比较多的方案。

怎么实现呢?

scala 复制代码
class VerifyPaymentPasswordView extends StatefulWidget {
  VerifyPaymentPasswordView({super.key, this.confirmAction});

  VoidCallback? confirmAction;

  @override
  State<VerifyPaymentPasswordView> createState() => _VerifyPaymentPasswordViewState();
}

class _VerifyPaymentPasswordViewState extends State<VerifyPaymentPasswordView> {
  final TextEditingController _controller = TextEditingController();
  final FocusNode _node = FocusNode();

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _node.requestFocus();
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedPadding(
      padding: MediaQuery.of(context).viewInsets,
      ...
    );
  }

效果同样能实现,但是由于带有动画的原因,顺滑度与响应速度上还是比不上上面的两种方案。

这里抛出一个疑问,为什么 AnimatedPadding 的 padding 可以直接使用一个 MediaQuery.of(context).viewInsets 值?有谁给他设置状态了吗?

其实并没有,只是在软键盘弹起的过程中,页面的 viewInsets 发生了改变,会触发 build 方法重新构建视图而已,所以每次 build 之后就可以拿到最新的 viewInsets 值,就能间接的实现设置状态的效果,也就能动啦。

那我直接用 Padding 岂不是更高效? 还避免了重复 build 的问题。

scala 复制代码
class ReviewDialog extends StatefulWidget {
  const ReviewDialog({Key? key}) : super(key: key);

  @override
  State<ReviewDialog> createState() => _ReviewDialogState();
}

class _ReviewDialogState extends State<ReviewDialog> {
  final _focusNode = FocusNode();
  EdgeInsets viewInsets = EdgeInsets.zero;

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _focusNode.requestFocus();
    });

    super.initState();
  }


  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
      ),
      child: Container(
        height: 100,
        width: double.infinity,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(10),
        ),
        alignment: Alignment.center,
        child: TextField(focusNode: _focusNode),
      ),
    );
  }
}

效果:

总结

害,早说啊,害我走那么多弯路!😅😅

其实如果只是做对应的弹窗效果的话,确实直接用 MediaQuery.of(context).viewInsets 相关的对象即可实现。

但是软键盘的状态监听与viewInsets的值监听在一些特殊的场景下是有作用的,比如当软键盘关闭的时候做出一些移除焦点,清空内容等等的特殊处理。

由于代码比较简单,本文全部贴出没有提供 Demo。由于 GIF 录制出来有压缩效果不明显,其实在 Debug 模式下效果是有明显区别的,有兴趣大家可以自行测试。也可以少走弯路直接使用最后的方案即可。

那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。

相关推荐
爱数学的程序猿2 小时前
Python入门:6.深入解析Python中的序列
android·服务器·python
brhhh_sehe2 小时前
重生之我在异世界学编程之C语言:深入文件操作篇(下)
android·c语言·网络
zhangphil2 小时前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆形图实现,Kotlin(2)
android·kotlin
Calvin8808282 小时前
Android Studio 的革命性更新:Project Quartz 和 Gemini,开启 AI 开发新时代!
android·人工智能·android studio
敲代码敲到头发茂密4 小时前
【大语言模型】LangChain 核心模块介绍(Memorys)
android·语言模型·langchain
H1005 小时前
重构(二)
android·重构
拓端研究室5 小时前
R基于贝叶斯加法回归树BART、MCMC的DLNM分布滞后非线性模型分析母婴PM2.5暴露与出生体重数据及GAM模型对比、关键窗口识别
android·开发语言·kotlin
zhangphil6 小时前
Android简洁缩放Matrix实现图像马赛克,Kotlin
android·kotlin
m0_512744646 小时前
极客大挑战2024-web-wp(详细)
android·前端
lw向北.6 小时前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt