Flutter的弹窗与软键盘交互

前言

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

干流的计划是加滚动布局,或许设置 Scaffold 的 resizeToAvoidBottomInset 特点,可是假如是弹窗呢?假如不做处理是什么状况?

上图:

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

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

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

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

一、软键盘开关监听

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

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

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,
      ...
    );
  }

作用:

假如是底部的弹窗呢?

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 的改动呢?

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),
      ),
    );
  }
}

这儿就不重复贴居中的弹窗布局了,是一样的监听计划。

居中弹窗作用:

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

底部弹窗作用:

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

作用比软键盘弹起与收起的监听的作用要稍好。

三、直接用AnimatedPadding或Padding

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

怎样完成呢?

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,
      ...
    );
  }

作用相同能完成,可是由于带有动画的原因,顺滑度与响应速度上仍是比不上上面的两种计划。

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

这儿抛出一个疑问,为什么 AnimatedPadding 的 padding 能够直接运用一个 MediaQuery.of(context).viewInsets 值?有谁给他设置状况了吗?

其实并没有,仅仅在软键盘弹起的过程中,页面的 viewInsets 发生了改动,会触发 build 方法重新构建视图罢了,所以每次 build 之后就能够拿到最新的 viewInsets 值,就能间接的完成设置状况的作用,也就能动啦。

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

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

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),
      ),
    );
  }
}

作用:

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

总结

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

其实假如仅仅做对应的弹窗作用的话,确实直接用 MediaQuery.of(context).viewInsets 相关的对象即可完成。

可是软键盘的状况监听与viewInsets的值监听在一些特别的场景下是有作用的,比方当软键盘封闭的时分做出一些移除焦点,清空内容等等的特别处理。

由于代码比较简单,本文全部贴出没有提供 Demo。由于 GIF 录制出来有压缩作用不显着,其实在 Debug 模式下作用是有显着差异的,有爱好我们能够自行测验。也能够少走弯路直接运用最终的计划即可。

那么本期内容就到这儿,如讲的不到位或错漏的当地,希望同学们能够谈论区指出。

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

Ok,这一期就此完结。

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