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,这一期就此完结。

相关推荐
Digitally1 小时前
如何将文件从 iPhone 传输到 Android(新指南)
android·ios·iphone
whysqwhw2 小时前
OkHttp深度架构缺陷分析与演进规划
android
用户7093722538512 小时前
Android14 SystemUI NotificationShadeWindowView 加载显示过程
android
木叶丸3 小时前
跨平台方案该如何选择?
android·前端·ios
nc_kai3 小时前
Flutter 之 每日翻译 PreferredSizeWidget
java·前端·flutter
顾林海3 小时前
Android ClassLoader加载机制详解
android·面试·源码
用户2018792831673 小时前
🎨 童话:Android画布王国的奇妙冒险
android
littlegnal4 小时前
Flutter Add-to-app profiling
flutter
whysqwhw4 小时前
OkHttp框架的全面深入架构分析
android
你过来啊你4 小时前
Android App冷启动流程详解
android