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

相关推荐
结局无敌1 小时前
Flutter状态管理实战:从新手到进阶的选型与落地指南
flutter
hh.h.2 小时前
开源鸿蒙生态下Flutter的发展前景分析
flutter·开源·harmonyos
遝靑2 小时前
Flutter 跨端开发进阶:可复用自定义组件封装与多端适配实战(移动端 + Web + 桌面端)
前端·flutter
三少爷的鞋3 小时前
Repository 方法设计:suspend 与 Flow 的决选择指南(以朋友圈为例)
android
Peng.Lei3 小时前
Flutter 常用命令大全
flutter
阿里云云原生3 小时前
Android App 崩溃排查指南:阿里云 RUM 如何让你快速从告警到定位根因?
android·java
ujainu4 小时前
Flutter与DevEco Studio混合开发:跨端状态同步技术规范与实战
flutter·deveco studio
ujainu4 小时前
Flutter 与 DevEco Studio 混合开发技术规范与实战指南
flutter·deveco studio
ujainu5 小时前
鸿蒙与Flutter:全场景开发的技术协同与价值
flutter·华为·harmonyos
cmdch20175 小时前
手持机安卓新增推送按钮功能
android