flutter 键盘弹出和收起页面bug

1,278 阅读3分钟

场景 在这里插入图片描述 我在客服系统中定义个列表widget和底层输入widget,但是在软键盘的弹出收起的时候,发现列表widget 并没有被重新绘制,这导致列表中的部分信息被遮挡, 1、Scaffold 首先想到的 Scaffold 的 resizeToAvoidBottomInset 属性。 在 Flutter 中 Scaffold 默认情况下 resizeToAvoidBottomInset 为 true,当 resizeToAvoidBottomInset 为 true 时,Scaffold 内部会将 mediaQuery.viewInsets.bottom 参与到 BoxConstraints 的大小计算,也就是键盘弹起时调整了内部的 bottom 位置来迎合键盘。这时候键盘已经收起,mediaQuery.viewInsets.bottom 应该更新为 0 ,那为何界面没有产生应有的更新呢?

MediaQuery MediaQuery是建立媒体查询解析给定数据的子树。 使用时是获取到MediaQueryData。所以只介绍MediaQueryData的属性

  • size 一个包含宽度和高度的对象,单位是dp(乘以密度就是你设备的像素)
  • devicePixelRatio 密度(像素比)
  • textScaleFactor 每个逻辑像素的字体像素数
  • platformBrightness 主机平台当前亮度模式
  • viewInsets 完全被系统UI(通常是设备的键盘)遮挡的显示部分
  • padding 我们通常取上边刘海高度和下边导航高度
  • alwaysUse24HourFormat 格式化时间时是否使用24小时格式
  • accessibleNavigation 用户是否使用TalkBack或VoiceOver等辅助功能服务与应用程序进行交互
  • invertColors 设备是否反转平台的颜色
  • disableAnimations 平台是否要求尽可能禁用或减少动画
  • boldText 平台是否请求使用粗体字体重绘制文本 MediaQuery 是一个 InheritedWidget,它会往下共享对应的 MediaQueryData,在 MediaQueryData 中保存了各种设备的信息,比如 size 、devicePixelRatio 、 textScaleFactor 、 viewPadding 以及 viewInsets 等。 那 viewInsets 是什么的呢?官方的解释是:

“可以被系统显示的区域,通常是和设备的键盘等相关,当键盘弹出时 viewInsets.bottom 对应的就是键盘的顶部。”

那上面的 bug 看起来可能就是 Scaffold 的 viewInsets.bottom 在键盘收起来时没有正常重置。

原因 Navigator 的相关逻辑,我们常用的 Navigator 其实是一个 StatefulWidget,当 MaterialApp 被更新时,可以看到在 NavigatorState 的 didUpdateWidget 回调中会调用 _history 里所有路由的 changedExternalState() 方法。

 @override
  void didUpdateWidget(Navigator oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.observers != widget.observers) {
      for (NavigatorObserver observer in oldWidget.observers)
        observer._navigator = null;
      for (NavigatorObserver observer in widget.observers) {
        assert(observer.navigator == null);
        observer._navigator = this;
      }
    }
    for (Route<dynamic> route in _history)
      route.changedExternalState();
  }
  

而 changedExternalState 执行后会调用 _forceRebuildPage 将路由里的 _page 清空,这样自然下次 Route 在 build 时触发的 PageRoute 重新 builder 方法。

@override
 void changedExternalState() {
   super.changedExternalState();
   if (_scopeKey.currentState != null)
     _scopeKey.currentState._forceRebuildPage();
 }
 
·····

 void _forceRebuildPage() {
   setState(() {
     _page = null;
   });
 }

这个 bug 首先是因为不规范使用了 MediaQueryData.fromWindow(WidgetsBinding.instance.window) ,之后又恰好在有键盘的页面打开后触发了 MaterialApp 的更新,导致了 PageRoute 重新 builder, 使得没有键盘的 Scaffold 使用了弹出键盘的 viewInsets.bottom。 所以这里只需要将 MediaQueryData.fromWindow 换成 MediaQuery.of(context) 就可以解决问题,而当在没有 context 或者需要直接使用 MediaQueryData.fromWindow 时,那一定要搭配上 WidgetsBindingObserver.didChangeMetrics 配合更新。 感谢郭神!!!!