Flutter 中键盘弹起时,Scaffold 发生了什么变化
最近刚好有网友咨询一个问题,那就顺便借着这个问题给大家深入介绍下 Flutter 中键盘弹起时,Scaffold
的内部发生了什么变化,让大家更好理解 Flutter 中的输入键盘和 Scaffold
的关系。
如下图所示,当时的问题是:当界面内有 TextField
输入框时,点击键盘弹起后,界面内底部的按键和 FloatButton 会被挤到键盘上面,有什么办法可以让底部按键和 FloatButton 不被顶上来吗?
其实解决这个问题很简单,那就是只要把 Scaffold
的 resizeToAvoidBottomInset
配置为 false
,结果如下图所示,键盘弹起后底部按键和 FloatButton 不会再被顶上来,问题解决。那为什么键盘弹起会和 resizeToAvoidBottomInset
有关系?
Scaffold 的 resize
Scaffold
是 Flutter 中最常用的页面脚手架,前面知道了通过 resizeToAvoidBottomInset
,我们可以配置在键盘弹起时页面的底部按键和 FloatButton 不会再被顶上来,其实这个行为是因为 Scaffold
的 body
大小被 resize
了。
那这个过程是怎么发生的呢?首先如下图所示,我们在 Scaffold
的源码里可以看到,当resizeToAvoidBottomInset
为 true 时,会使用 mediaQuery.viewInsets.bottom
作为 minInsets
的参数,也就是可以确定:键盘弹起时的界面 resize
和 mediaQuery.viewInsets.bottom
有关系。
而如下图所示, Scaffold
内部的布局主要是靠 CustomMultiChildLayout
,CustomMultiChildLayout
的布局逻辑主要在 MultiChildLayoutDelegate
对象里。
前面获取到的 minInsets
会被用到 _ScaffoldLayout
这个 MultiChildLayoutDelegate
里面,也就是说 Scaffold
的内部是通过 CustomMultiChildLayout
实现的布局,具体实现逻辑在 _ScaffoldLayout
这个 Delegate
里。
关于
CustomMultiChildLayout
的详细使用介绍在之前的文章 《详解自定义布局实战》 里可以找到。
接着看 _ScaffoldLayout
, 在 _ScaffoldLayout
进行布局时,会通过传入的
minInsets
来决定 body
显示的 contentBottom
, 所以可以看到事实上传入的 minInsets
改变的是 Scaffold
布局的 bottom 位置。
上图代码中使用的
_ScaffoldSlot.body
这个枚举其实是作为LayoutId
的值,MultiChildLayoutDelegate
在布局时可以通过LayoutId
获取到对应 child 进行布局操作,详细可见: 《详解自定义布局实战》
那么 Scaffold
的 body
是什么呢? 如上图代码所示,其实 Scaffold
的 body
是一个叫 _BodyBuilder
的对象,而这个 _BodyBuilder
内部其实是一个 LayoutBuilder
。(注意,在 widget.appbar
不为 null
时,会 removeTopPadding
)
所以如下图代码所示 body
在添加时,它父级的MediaQueryData
会被重载,特别是 removeTopPadding
会被清空,viewInsets.bottom
也是会被重置。
最后如下代码所示,_BodyBuilder
的 LayoutBuilder
里会获取到一个 top
和 bottom
的参数,这两个参数都通过前面在 _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();}
}
如上代码所示:
- 代码中定义了
MainWidget
和CustomWidget
两个控件; MainWidget
里使用了Scaffold
,并且CustomWidget
在MainWidget
里被使用;- 分别在这两个 Widget 的
build
方法里打印出对应的MediaQuery.of(context).padding
和MediaQuery.of(context).viewInsets.bottom
的值;
如下图所示,在键盘弹起和不弹起时可以看到 padding
值是不同的,而 viewInsets.bottom
都为 0。
为什么 padding
值的 top
会不一致,自然是因为 CustomWidget
和 MainWidget
获取到的 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
内被重载清除了。
总结一下:Scaffold
的 resizeToAvoidBottomInset
会通过 MediaQueryData
影响 body 的布局,同时在 Scaffold
内 MediaQuery
会被重载,所以使用的 context
位置不同,获取到的 MediaQueryData
也不同,如果需要获取键盘高度和状态栏高度的话,最好使用 Scaffold
外的 context
。
这里讲了
MediaQuery
和MediaQueryData
的内容,为什么MediaQuery
通过嵌套就可以重载?为什么通过context
可以往上获取到离context
最近的MediaQueryData
?因为MediaQuery
是一个InheritedWidget
: 《全面理解State》 。
键盘如何影响 Scaffold
前面我们聊了 Scaffold
的 resizeToAvoidBottomInset
会通过 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
,最终改变了 Scaffod
的 body
大小。
那么到这里,你知道如何在 Flutter 里正确地去获取键盘的高度了吧?
最后
从一个简单的 resizeToAvoidBottomInset
去拓展到 Scaffod
的内部布局和 MediaQueryData
与键盘的关系,其实这也是学习框架过程中很好的知识延伸,通过特定的问题去深入理解框架的实现原理,最后再把知识点和问题关联起来,这样问题在此之后便不再是问题,因为入脑了~
Flutter 中键盘弹起时,Scaffold 发生了什么变化相关推荐
- 安卓和iOS的兼容性问题: 键盘弹起时,固定在底部的按钮是否被弹到键盘上方
问题描述: iOS不存在固定在底部的按钮被键盘弹起的问题,该问题出现在安卓手机上,如下图 解决方法: 1. 当键盘弹起时可以监听窗口变化(window.onresize 监听窗口变化) 2. 窗口发生 ...
- 解决uni-app微信小程序底部input输入框,键盘弹起时页面整体上移问题
一.存在的问题: 微信小程序聊天界面,当input 框获取焦点时会自动调起手机键盘,当键盘弹起时,会导致页面整体上移,页面头信息会消失不见. 二.需要实现的效果 键盘弹出时, 底部的 ...
- Flutter中使用Rive时出现闪退问题可能原因
Flutter中使用Rive时出现闪退问题可能是由于以下几个原因: Rive文件格式问题:Rive文件格式不兼容或者文件本身有问题.可以尝试重新导出Rive文件或者使用其他版本的Rive文件进行测试. ...
- 【QT】震惊,一个由于QT只有.pro文件引起的世界难题。本文解决QT只有.pro的问题以及在项目中添加文件时,发生了一个编码错误的问题。
震惊,一个由于QT只有.pro文件引起的世界难题!! 新手必看,避雷!!!不要相信网上那些人,他们文章中看不中用,正所谓印证了网络上的一句话:一人创作,万人模仿啊.和某手某音差不多!!# 概述:问题的 ...
- 解决uni-app微信小程序input输入框在底部时,键盘弹起页面整体上移问题
问题描述: 最近的做了个客服聊天的功能,遇到一个问题如下: 在手机上点击聊天页底部的input框后,键盘弹起同时页面会整体上移,标题栏被顶上去了.如下图: 问题分析: input 获取焦点时会自动调起 ...
- 处理ios软键盘弹起和收起时页面滚动问题
处理ios软键盘弹起和收起时页面滚动问题 背景: 在开发添加主播功能时,页面底部需要弹出一个抽屉弹窗,点击抽屉上的输入框,会唤起软键盘,由于iOS的软键盘触发方式是将页面滚动,所以导致页面位置偏移 b ...
- 在Flutter中解析复杂的JSON(一篇顶十篇)
文章目录 JSON结构#1:简单 map 访问对象 Snippet #1 : imports **Snippet #2 : **加载Json Asset(可选)** Snippet #3 : 加载响应 ...
- h5 移动端 监听软键盘弹起、收起
前面一篇博客 h5 安卓 键盘弹起界面适配 修改webview高度提到了在adnroid中如何监听软键盘的弹起与收起,是利用的窗口的高度发生变化 window.onresize事件来做突破点的,但是i ...
- Flutter中如何选择StatelessWidget和StatefulWidget
目录 StatelessWidget和StatefulWidget的区别 StatelessWidget StatefulWidget 区别 什么情况下应该用StatelessWidget?什么情况下 ...
最新文章
- QTP中对数据库的操作(查询,更新和删除等)
- SSL加密包解析的几个概念梳理
- 疯狂.NET架构通用权限后台管理工具演示版2.0下载
- 元宇宙系列白皮书——未来已来:全球XR产业洞察
- 领导开会为什么总爱在桌子上摆一个水杯?
- 免费开源:人人必备的数据分析技能
- Microsoft ASP.NET 4 Step by Step
- 人工智能python营_AI人工智能训练营
- CEEMDAN算法及其应用
- 支付宝手机网站支付、支付查询、退款、退款查询、转账接口整合
- Final Cut Pro中文教程 (1) 基础认识Final Cut Pro
- Android有线投屏实践
- java response 输出word_如何使用java代码导出word
- Web.config详解+asp.net优化
- 有五只猴子分一堆桃子.第一只猴子最先来,扔了一个后平分成五分,拿走了一份.其他猴子也一样扔了一个,平分成5分,拿走自己的.问:桃子至少有几个?
- 人类简史:精华语录 (尤瓦尔·赫拉利)
- Photoshop CS5软件
- 【Unity Shader】设置uniform
- QVGA HVGA WVGA
- Ubuntu16.04安装Windows可执行文件(QQ.exe)