最近刚好有网友咨询一个问题,那就顺便借着这个问题给大家深入介绍下 Flutter 中键盘弹起时,Scaffold 的内部发生了什么变化,让大家更好理解 Flutter 中的输入键盘和 Scaffold 的关系。

如下图所示,当时的问题是:当界面内有 TextField 输入框时,点击键盘弹起后,界面内底部的按键和 FloatButton 会被挤到键盘上面,有什么办法可以让底部按键和 FloatButton 不被顶上来吗?

其实解决这个问题很简单,那就是只要ScaffoldresizeToAvoidBottomInset 配置为 false ,结果如下图所示,键盘弹起后底部按键和 FloatButton 不会再被顶上来,问题解决。那为什么键盘弹起会和 resizeToAvoidBottomInset 有关系?

Scaffold 的 resize

Scaffold 是 Flutter 中最常用的页面脚手架,前面知道了通过 resizeToAvoidBottomInset ,我们可以配置在键盘弹起时页面的底部按键和 FloatButton 不会再被顶上来,其实这个行为是因为 Scaffoldbody 大小被 resize 了。

那这个过程是怎么发生的呢?首先如下图所示,我们在 Scaffold 的源码里可以看到,当resizeToAvoidBottomInset 为 true 时,会使用 mediaQuery.viewInsets.bottom 作为 minInsets 的参数,也就是可以确定:键盘弹起时的界面 resizemediaQuery.viewInsets.bottom 有关系

而如下图所示, Scaffold 内部的布局主要是靠 CustomMultiChildLayoutCustomMultiChildLayout 的布局逻辑主要在 MultiChildLayoutDelegate 对象里。

前面获取到的 minInsets 会被用到 _ScaffoldLayout 这个 MultiChildLayoutDelegate 里面,也就是说 Scaffold 的内部是通过 CustomMultiChildLayout 实现的布局,具体实现逻辑在 _ScaffoldLayout 这个 Delegate

关于 CustomMultiChildLayout 的详细使用介绍在之前的文章 《详解自定义布局实战》 里可以找到。

接着看 _ScaffoldLayout , 在 _ScaffoldLayout 进行布局时,会通过传入的
minInsets 来决定 body 显示的 contentBottom , 所以可以看到事实上传入的 minInsets 改变的是 Scaffold 布局的 bottom 位置

上图代码中使用的 _ScaffoldSlot.body 这个枚举其实是作为 LayoutId 的值,MultiChildLayoutDelegate 在布局时可以通过 LayoutId 获取到对应 child 进行布局操作,详细可见: 《详解自定义布局实战》

那么 Scaffoldbody 是什么呢? 如上图代码所示,其实 Scaffoldbody 是一个叫 _BodyBuilder 的对象,而这个 _BodyBuilder 内部其实是一个 LayoutBuilder。(注意,在 widget.appbar 不为 null 时,会 removeTopPadding

所以如下图代码所示 body 在添加时,它父级的MediaQueryData 会被重载,特别是 removeTopPadding 会被清空,viewInsets.bottom 也是会被重置

最后如下代码所示,_BodyBuilderLayoutBuilder 里会获取到一个 topbottom 的参数,这两个参数都通过前面在 _ScaffoldLayout 布局时传入的 constraints 去判断得到,最终 copyWith 得到新的 MediaQuery

这里就涉及到一个有意思的点,在 _BodyBuilder 里的通过 copyWith 得到新的 MediaQuery 会影响什么呢?如下代码所示,这里用一个简单的例子来解释下。

class MainWidget extends StatelessWidget {final TextEditingController controller =new TextEditingController(text: "init Text");@overrideWidget build(BuildContext context) {print("Main MediaQuery padding: ${MediaQuery.of(context).padding} viewInsets.bottom: ${MediaQuery.of(context).viewInsets.bottom}");return Scaffold(appBar: AppBar(title: new Text("MainWidget"),),extendBody: true,body: Column(children: [new Expanded(child: InkWell(onTap: (){FocusScope.of(context).requestFocus(FocusNode());})),///增加 CustomWidgetCustomWidget(),new Container(margin: EdgeInsets.all(10),child: new Center(child: new TextField(controller: controller,),),),new Spacer(),],),);}
}
class CustomWidget extends StatelessWidget {@overrideWidget build(BuildContext context) {print("Custom MediaQuery padding: ${MediaQuery.of(context).padding} viewInsets.bottom: ${MediaQuery.of(context).viewInsets.bottom}\n  \n");return Container();}
}

如上代码所示:

  • 代码中定义了 MainWidgetCustomWidget 两个控件;
  • MainWidget 里使用了 Scaffold ,并且 CustomWidgetMainWidget 里被使用;
  • 分别在这两个 Widget 的build 方法里打印出对应的 MediaQuery.of(context).paddingMediaQuery.of(context).viewInsets.bottom 的值;

如下图所示,在键盘弹起和不弹起时可以看到 padding 值是不同的,而 viewInsets.bottom 都为 0。

为什么 padding 值的 top 会不一致,自然是因为 CustomWidgetMainWidget获取到的 MediaQuery.of(context) 对象不是同一个数据。

  • MainWidget 使用的 MediaQuery.of(context) 得到的 MediaQueryData 是上级往下传递的,里面包含了 top:47 的状态栏高度和 bottom:34 的底部安全区域高度

  • CustomWidget 里面 MediaQuery.of(context) 得到的 MediaQueryData ,自然就是前面分析过的 _BodyBuilder 里的通过 copyWith 得到新的 MediaQuery,所以 CustomWidget 得到的 MediaQueryData 其实Scaffold 内部已经被重置了,所以它的 top:0 ,获取不到状态栏高度

事实上这就是大家为什么有时候 MediaQuery.of( context) 可以获取到状态栏高度,有时候又获取不到的原因,因为你的 context 获取到的是 Scaffold 之外的 MediaQueryData, 还是 Scaffold 内被重载过的 MediaQueryData,自然会得到不一样的结果。

如下图所示,键盘弹起因为被 resize 了,所以界面的 bottom 安全区域变成了 0 ,而

  • MainWidget 中可以获取到 viewInsets.bottom 也就是键盘的高度;
  • CustomWidget 获取不到 viewInsets.bottom ,因为在 Scaffold 内被重载清除了。

总结一下:ScaffoldresizeToAvoidBottomInset 会通过 MediaQueryData 影响 body 的布局,同时在 ScaffoldMediaQuery 会被重载,所以使用的 context 位置不同,获取到的 MediaQueryData 也不同,如果需要获取键盘高度和状态栏高度的话,最好使用 Scaffold 外的 context

这里讲了 MediaQueryMediaQueryData 的内容,为什么 MediaQuery 通过嵌套就可以重载?为什么通过 context 可以往上获取到离 context 最近的 MediaQueryData?因为 MediaQuery 是一个 InheritedWidget : 《全面理解State》 。

键盘如何影响 Scaffold

前面我们聊了 ScaffoldresizeToAvoidBottomInset 会通过 MediaQueryData 影响 body 的布局,那是怎么影响的呢?

事实上这得从 MaterialApp 说起,在 MaterialApp 内部的深处嵌套着一个叫 _MediaQueryFromWindow 的 Widget ,它在内部通过 WidgetsBinding.instance.addObserver 对 App 的各种系统事件做了监听,并且对应都执行了 setState

所以如下源码所示,当键盘弹出时, build 方法会被执行, 而 MediaQueryData 就会通过MediaQueryData.fromWindow 获取到新的 MediaQueryData 数据。

 @overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);}// ACCESSIBILITY@overridevoid didChangeAccessibilityFeatures() {setState(() { });}// METRICS@overridevoid didChangeMetrics() {setState(() {});}@overridevoid didChangeTextScaleFactor() {setState(() { });}// RENDERING@overridevoid didChangePlatformBrightness() {setState(() {});}@overrideWidget build(BuildContext context) {MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);if (!kReleaseMode) {data = data.copyWith(platformBrightness: debugBrightnessOverride);}return MediaQuery(data: data,child: widget.child,);}@overridevoid dispose() {WidgetsBinding.instance.removeObserver(this);super.dispose();}

举个例子,如下图所示,从 Android 的 Java 层弹出键盘开始,会把改变后的视图信息传递给 C++ 层,最后回调到 Dart 层,从而触发 MaterialApp 内的 didChangeMetrics 方法执行 setState(() {}); ,进而让 _MediaQueryFromWindow 内的 build 更新了 MediaQueryData ,最终改变了 Scaffodbody 大小。

那么到这里,你知道如何在 Flutter 里正确地去获取键盘的高度了吧?

最后

从一个简单的 resizeToAvoidBottomInset 去拓展到 Scaffod 的内部布局和 MediaQueryData 与键盘的关系,其实这也是学习框架过程中很好的知识延伸,通过特定的问题去深入理解框架的实现原理,最后再把知识点和问题关联起来,这样问题在此之后便不再是问题,因为入脑了~

Flutter 中键盘弹起时,Scaffold 发生了什么变化相关推荐

  1. 安卓和iOS的兼容性问题: 键盘弹起时,固定在底部的按钮是否被弹到键盘上方

    问题描述: iOS不存在固定在底部的按钮被键盘弹起的问题,该问题出现在安卓手机上,如下图 解决方法: 1. 当键盘弹起时可以监听窗口变化(window.onresize 监听窗口变化) 2. 窗口发生 ...

  2. 解决uni-app微信小程序底部input输入框,键盘弹起时页面整体上移问题

    一.存在的问题:          微信小程序聊天界面,当input 框获取焦点时会自动调起手机键盘,当键盘弹起时,会导致页面整体上移,页面头信息会消失不见. 二.需要实现的效果 键盘弹出时, 底部的 ...

  3. Flutter中使用Rive时出现闪退问题可能原因

    Flutter中使用Rive时出现闪退问题可能是由于以下几个原因: Rive文件格式问题:Rive文件格式不兼容或者文件本身有问题.可以尝试重新导出Rive文件或者使用其他版本的Rive文件进行测试. ...

  4. 【QT】震惊,一个由于QT只有.pro文件引起的世界难题。本文解决QT只有.pro的问题以及在项目中添加文件时,发生了一个编码错误的问题。

    震惊,一个由于QT只有.pro文件引起的世界难题!! 新手必看,避雷!!!不要相信网上那些人,他们文章中看不中用,正所谓印证了网络上的一句话:一人创作,万人模仿啊.和某手某音差不多!!# 概述:问题的 ...

  5. 解决uni-app微信小程序input输入框在底部时,键盘弹起页面整体上移问题

    问题描述: 最近的做了个客服聊天的功能,遇到一个问题如下: 在手机上点击聊天页底部的input框后,键盘弹起同时页面会整体上移,标题栏被顶上去了.如下图: 问题分析: input 获取焦点时会自动调起 ...

  6. 处理ios软键盘弹起和收起时页面滚动问题

    处理ios软键盘弹起和收起时页面滚动问题 背景: 在开发添加主播功能时,页面底部需要弹出一个抽屉弹窗,点击抽屉上的输入框,会唤起软键盘,由于iOS的软键盘触发方式是将页面滚动,所以导致页面位置偏移 b ...

  7. 在Flutter中解析复杂的JSON(一篇顶十篇)

    文章目录 JSON结构#1:简单 map 访问对象 Snippet #1 : imports **Snippet #2 : **加载Json Asset(可选)** Snippet #3 : 加载响应 ...

  8. h5 移动端 监听软键盘弹起、收起

    前面一篇博客 h5 安卓 键盘弹起界面适配 修改webview高度提到了在adnroid中如何监听软键盘的弹起与收起,是利用的窗口的高度发生变化 window.onresize事件来做突破点的,但是i ...

  9. Flutter中如何选择StatelessWidget和StatefulWidget

    目录 StatelessWidget和StatefulWidget的区别 StatelessWidget StatefulWidget 区别 什么情况下应该用StatelessWidget?什么情况下 ...

最新文章

  1. QTP中对数据库的操作(查询,更新和删除等)
  2. SSL加密包解析的几个概念梳理
  3. 疯狂.NET架构通用权限后台管理工具演示版2.0下载
  4. 元宇宙系列白皮书——未来已来:全球XR产业洞察
  5. 领导开会为什么总爱在桌子上摆一个水杯?
  6. 免费开源:人人必备的数据分析技能
  7. Microsoft ASP.NET 4 Step by Step
  8. 人工智能python营_AI人工智能训练营
  9. CEEMDAN算法及其应用
  10. 支付宝手机网站支付、支付查询、退款、退款查询、转账接口整合
  11. Final Cut Pro中文教程 (1) 基础认识Final Cut Pro
  12. Android有线投屏实践
  13. java response 输出word_如何使用java代码导出word
  14. Web.config详解+asp.net优化
  15. 有五只猴子分一堆桃子.第一只猴子最先来,扔了一个后平分成五分,拿走了一份.其他猴子也一样扔了一个,平分成5分,拿走自己的.问:桃子至少有几个?
  16. 人类简史:精华语录 (尤瓦尔·赫拉利)
  17. Photoshop CS5软件
  18. 【Unity Shader】设置uniform
  19. QVGA HVGA WVGA
  20. Ubuntu16.04安装Windows可执行文件(QQ.exe)

热门文章

  1. matlab生成随机数小结,MATLAB生成随机数总结
  2. 11.10 VLAN实验总结
  3. Go语言基础学习笔记
  4. web安全Wargame—Natas解题思路(1-26)
  5. 怎么给html页面添加网格线,如何在HTML中创建2×2网格表格
  6. 2021双11促销短信方案参考
  7. 服务网格和 Istio 简介
  8. SAP Hybris 123 官方网站视频讲解
  9. 领域驱动设计--领域驱动设计到数据建模实践(十)
  10. 控制台应用于指挥中心的技术要求是什么?