当永恒的软键盘问题遇到Flutter

2022-05-10 20:39:36 浏览数 (1)

移动端开发的同学可能或多或少都遇到过软键盘的问题。不是被遮住布局就是布局顶不上去。那么使用 Flutter 的时候,遇到软键盘出来的时候又会遇到什么问题呢?最近在练习使用 Flutter,顺便撸个自己的 APP,遇到了这个问题,把自己的实践顺便拿出来分享一下。

从场景开始说起

我的场景是一个从底部弹出的 Dialog,Dialog 里主要就是一个 TextField 输入框。如图:

这个时候当 TextInput 获得输入焦点的时候,情况出现了:

这里会直接类似这种报错。

贴一下异常堆栈看一下

代码语言:javascript复制
════════ Exception caught by rendering library ═════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 17 pixels on the bottom.

The relevant error-causing widget was: 
  Column file:///Users/chenglei/fataccount/lib/keepAccount/add_keep_account.dart:48:22
The overflowing RenderFlex has an orientation of Axis.vertical.
The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and black striped pattern. This is usually caused by the contents being too big for the RenderFlex.

哦!原来是布局溢出了,再仔细看看,会发现,当键盘弹出来的时候,正常布局就是在键盘的上面,留给dialog 可以用的就只有一点点高度了,自然就 over 了。

Google解决法

搜索了一下,发现 Flutter 中关于这个问题有一个属性可以解决,在所在页面的 Scaffold 设置一个 resizeToAvoidBottomInset 属性。

代码语言:javascript复制
resizeToAvoidBottomInset: false

看一下效果:

我们可以看到,布局确实不溢出了,但是我们的 Dialog 也看不到了。我们看下 resizeToAvoidBottomInset 的注释:

代码语言:javascript复制
/// If true the [body] and the scaffold's floating widgets should size
  /// themselves to avoid the onscreen keyboard whose height is defined by the
  /// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property.
  ///
  /// For example, if there is an onscreen keyboard displayed above the
  /// scaffold, the body can be resized to avoid overlapping the keyboard, which
  /// prevents widgets inside the body from being obscured by the keyboard.
  ///
  /// Defaults to true.

大概意思就是这个属性 true 的时候,布局会根据键盘高度去调整,避免自己被键盘挡住。那么是 false 的时候,就不会调整了。像我的这种在底部的输入框,就直接被键盘遮住了。

解决思路

那么既然底部对话框里面有输入框的时候,resize布局和不resize布局都不合适的时候,那么就只能考虑调整对话框自己的位置了。也就是,当键盘没弹出的时候,输入框在下面,键盘出来的时候,输入框在键盘的上方。底部对话框再怎么样,也不能被输入框顶到屏幕外面去吧。

这时候就有问题了:

  • 如何监听键盘弹出和收回
  • 如何根据键盘弹出收回来调整对话框的高度

根据上文 resizeToAvoidBottomInset 的注释,我们可以找到一个有用的信息, 键盘高度是可以从

代码语言:javascript复制
MediaQueryData.viewInsets

里面获取的。至于怎么监听键盘呢,其实 Google 一下也很简单,套用一下别人的思路:

界面的布局大小发生变化的时候,键盘高度不是0,我们就认为键盘弹出,反之键盘已经被收回。

至于如何监听界面大小变化了呢?

给你的 StatefulWidgetState 继承一个 WidgetsBindingObserver, 在 didChangeMetrics 方法里面就可以收到应用界面大小变化的回调了。

实践

既然大体思路有了,那么我们一步步来实践完成 Dialog 高度的兼容。

didChangeMetrics 回调里面,我们在当前 frame 结束的时候根据不同的高度来设置对话框的高度, 这里我准备了一个 initHeight 来表示对话框的初始高度:

代码语言:javascript复制
@override
  void didChangeMetrics() {
    super.didChangeMetrics();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      var bottom = MediaQuery.of(context).viewInsets.bottom;
      if (bottom == 0) {
        // 键盘收起来
        setState(() {
          this.height = initHeight;
        });
      } else {
        setState(() {
          this.height = initHeight   bottom;
        });
      }
    });
  }

至于初始高度的获取,也很简单,在 init 的时候,获取当前 Widget 的高度:

代码语言:javascript复制
@override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      initHeight = context.size.height;
    });
  }

这个时候运行一下,就会发现当键盘弹出的时候,输入框在键盘的上方。

但是在以为已经大功告成的时候,遇到了一个新问题,输入框的高度是可以随着输入的时候按了回车键之后变化的。我们直接按几个换行:

输入框的高度变大了,Dialog的高度没有变,输入框的下半部分仍然会被遮住。纠结了一会,想想还是再优化一下吧,似乎也不是很复杂。随时拿到输入框的高度,把高度的变化通知给 Dialog 就可以了。

优化

首先我需要随时能感知到输入框的高度,那么最实在的就是在输入的时候顺便监听一下输入框自己的 height,我选择自己封装了一个 Widget:

代码语言:javascript复制
final ValueChanged<double> heightChange;
final ValueChanged<double> initHeight;

@override
  Widget build(BuildContext context) {
    return TextField(
      onChanged: (content) {
        _checkHeight(context);
      },
      keyboardType: TextInputType.multiline,
    );
  }


_checkHeight(BuildContext context) {
    if (widget.heightChange != null) {
      widget.heightChange(context.size.height);
    }
  }

这个时候我们就能自己判断到对话框是不是在输入内容的时候变化了高度:

代码语言:javascript复制
/// 检查输入框的高度
/// 使用一个 lastInputHeight 变量来记录上一次的键盘高度
  _checkHeight(height) {
    if (height > lastInputHeight) {
      // 输入框行数多了
      initHeight  = (height - lastInputHeight);
      setState(() {
        var shouldHeight = this.height   (height - lastInputHeight);
        this.height = shouldHeight;
      });
    } else {
      initHeight -= (lastInputHeight - height);
      setState(() {
        var shouldHeight = this.height - (lastInputHeight - height);
        this.height = shouldHeight;
      });
    }
    lastInputHeight = height;
  }

这个时候,我就完成了 “高度 = 键盘高度 对话框高度 对话框高度变化值” 的逻辑。这时候再来看看效果:

总结

总结一下这里遇到的几个很有用的知识点:

如何获取一个 Widget 的高度?

Flutter 因为是响应式的布局开发,和 Android 这种命令式开发一个很大的区别就是基本避免直接操作一个 ui 的元素,这时候会遇到 2 个问题

  • 如何获取宽高
  • build的时候元素还没渲染完毕,又如何获取宽高

Flutter 中我们可以使用 context 去获取:

代码语言:javascript复制
context.size.height

或者

代码语言:javascript复制
(context.findRenderObject() as RenderBox).size.height

虽然 build 的时候我们无法判断宽高,但是我们可以监听渲染完毕的时候。自然就能想到使用 WidgetsBindingaddPostFrameCallback 方法,在当前 frame 完成的时候去调用,肯定是可以拿到宽高的。这个就非常类似 Android 中的 View.post{} 了。


如何获取键盘高度*

代码语言:javascript复制
MediaQuery.of(context).viewInsets.bottom;

这就是个简单的 API 问题了,严格来说这个获取的方式是系统底部的ui高度,但是基本 99% 的情况可以和软键盘划等号了。


WidgetsBindingObserver的使用

注册 Widget 层绑定的接口,各种行为的监听。包括:

didPushRoute : 路由跳转

didChangeMetrics : 应用旋转,屏幕大小变化

didChangeTextScaleFactor : 字体变化

didChangePlatformBrightness: 亮度变化

didChangeAppLifecycleState: 生命周期

didChangeLocales: 系统本地设置变化,例如时间,语言

didHaveMemoryPressure: 内存不足

didChangeAccessibilityFeatures: accessibility相关

我们可以根据自己的需求灵活利用这个接口去实现我们想要的功能。


本篇文章我分享了最近一次使用 Flutter 遇到软件盘的时候的处理方法。虽然回头看看思路整体不算很难,但是因为不熟悉,解决这个问题还是一波三折,花了一晚上的时间。这里拿出来分享一下,如果有朋友有更好的解决思路,也欢迎交流分享。

0 人点赞