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

相关推荐
晓梦林7 小时前
cp520靶场学习笔记
android·笔记·学习
SoaringHeart9 小时前
Flutter进阶:OverlayEntry 插入图层管理器 NOverlayZIndexManager
前端·flutter
有味道的男人10 小时前
Open Claw对接1688平台
android·rxjava
_李小白11 小时前
【android opencv学习笔记】Day 17: 目标追踪(MeanShift)
android·opencv·学习
用户860225046747211 小时前
AI 分析头部APP系统优化框架
android
用户860225046747211 小时前
AI分析头部APP优化框架
android
程序员老刘13 小时前
Flutter 3.44 有哪些变化?(官方blog完整翻译)
flutter·ai编程·客户端
2501_9160074714 小时前
iOS开发中抓取HTTPS请求的完整解决方法与步骤详解
android·网络协议·ios·小程序·https·uni-app·iphone
山屿落星辰15 小时前
Flutter 企业级架构设计实战:Clean Architecture + 分层模块化 + 依赖注入全解析
flutter
山屿落星辰17 小时前
Flutter 高级特性实战:动画、自定义绘制、平台通道与 Web 优化
前端·flutter