GSYFlutterBook/Flutter-KEY.md

228 lines
10 KiB
Markdown
Raw Permalink Normal View History

2021-04-29 15:59:26 +08:00
最近刚好有网友咨询一个问题,那就顺便借着这个问题给大家深入介绍下 Flutter 中键盘弹起时,`Scaffold` 的内部发生了什么变化,让大家更好理解 Flutter 中的输入键盘和 `Scaffold` 的关系。
如下图所示,当时的问题是:*当界面内有 `TextField` 输入框时,点击键盘弹起后,界面内底部的按键和 FloatButton 会被挤到键盘上面,有什么办法可以让底部按键和 FloatButton 不被顶上来吗?*
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image1)
其实解决这个问题很简单,那就是只要**把 `Scaffold``resizeToAvoidBottomInset` 配置为 `false`** ,结果如下图所示,键盘弹起后底部按键和 FloatButton 不会再被顶上来,问题解决。**那为什么键盘弹起会和 `resizeToAvoidBottomInset` 有关系?**
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image2)
### Scaffold 的 resize
`Scaffold` 是 Flutter 中最常用的页面脚手架,前面知道了通过 `resizeToAvoidBottomInset` ,我们可以配置在键盘弹起时页面的底部按键和 FloatButton 不会再被顶上来,其实这个行为是因为 `Scaffold``body` 大小被 `resize` 了。
那这个过程是怎么发生的呢?首先如下图所示,我们在 `Scaffold` 的源码里可以看到,当`resizeToAvoidBottomInset` 为 true 时,会使用 `mediaQuery.viewInsets.bottom` 作为 `minInsets` 的参数,也就是可以确定:**键盘弹起时的界面 `resize``mediaQuery.viewInsets.bottom` 有关系**。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image3)
而如下图所示, `Scaffold` 内部的布局主要是靠 `CustomMultiChildLayout` `CustomMultiChildLayout` 的布局逻辑主要在 `MultiChildLayoutDelegate` 对象里。
前面获取到的 `minInsets` 会被用到 `_ScaffoldLayout` 这个 `MultiChildLayoutDelegate` 里面,也就是说 **`Scaffold` 的内部是通过 `CustomMultiChildLayout` 实现的布局,具体实现逻辑在 `_ScaffoldLayout` 这个 `Delegate` 里**。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image4)
> 关于 `CustomMultiChildLayout` 的详细使用介绍在之前的文章 [《详解自定义布局实战》](https://juejin.cn/post/6844903878509461518#heading-10) 里可以找到。
接着看 `_ScaffoldLayout` `_ScaffoldLayout` 进行布局时,会通过传入的
`minInsets` 来决定 `body` 显示的 `contentBottom` 所以可以看到**事实上传入的 `minInsets` 改变的是 `Scaffold` 布局的 bottom 位置**。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image5)
> 上图代码中使用的 `_ScaffoldSlot.body` 这个枚举其实是作为 `LayoutId` 的值,`MultiChildLayoutDelegate` 在布局时可以通过 `LayoutId` 获取到对应 child 进行布局操作,详细可见: [《详解自定义布局实战》](https://juejin.cn/post/6844903878509461518#heading-10)
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image6)
那么 `Scaffold``body` 是什么呢? 如上图代码所示,其实 `Scaffold``body` 是一个叫 `_BodyBuilder` 的对象,而这个 `_BodyBuilder` 内部其实是一个 `LayoutBuilder`。(注意,在 `widget.appbar` 不为 `null` 时,会 `removeTopPadding`
所以如下图代码所示 `body` 在添加时,**它父级的`MediaQueryData` 会被重载,特别是 `removeTopPadding` 会被清空,`viewInsets.bottom` 也是会被重置**。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image7)
最后如下代码所示,`_BodyBuilder` 的 `LayoutBuilder` 里会获取到一个 `top``bottom` 的参数,这两个参数都通过前面在 `_ScaffoldLayout` 布局时传入的 `constraints` 去判断得到,最终 `copyWith` 得到新的 `MediaQuery`
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image8)
这里就涉及到一个有意思的点,在 `_BodyBuilder` 里的通过 `copyWith` 得到新的 `MediaQuery` 会影响什么呢?如下代码所示,这里用一个简单的例子来解释下。
```dart
class MainWidget extends StatelessWidget {
final TextEditingController controller =
new TextEditingController(text: "init Text");
@override
Widget 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());
})),
///增加 CustomWidget
CustomWidget(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(
controller: controller,
),
),
),
new Spacer(),
],
),
);
}
}
class CustomWidget extends StatelessWidget {
@override
Widget 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。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image9)
为什么 `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` 内被重载清除了。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image10)
总结一下:**`Scaffold` 的 `resizeToAvoidBottomInset` 会通过 `MediaQueryData` 影响 body 的布局,同时在 `Scaffold``MediaQuery` 会被重载,所以使用的 `context` 位置不同,获取到的 `MediaQueryData` 也不同,如果需要获取键盘高度和状态栏高度的话,最好使用 `Scaffold` 外的 `context` 。**
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image11)
> 这里讲了 `MediaQuery` 和 `MediaQueryData` 的内容,为什么 `MediaQuery` 通过嵌套就可以重载?为什么通过 `context` 可以往上获取到离 `context` 最近的 `MediaQueryData`?因为 `MediaQuery` 是一个 `InheritedWidget` : [《全面理解State》](https://juejin.cn/post/6844903866706706439#heading-5) 。
### 键盘如何影响 Scaffold
前面我们聊了 `Scaffold``resizeToAvoidBottomInset` 会通过 `MediaQueryData` 影响 body 的布局,那是怎么影响的呢?
事实上这得从 `MaterialApp` 说起,在 `MaterialApp` 内部的深处嵌套着一个叫 `_MediaQueryFromWindow` 的 Widget ,它在内部通过 ` WidgetsBinding.instance.addObserver` 对 App 的各种系统事件做了监听,并且对应都执行了 `setState`
所以如下源码所示,当键盘弹出时, `build` 方法会被执行, 而 `MediaQueryData` 就会通过`MediaQueryData.fromWindow` 获取到新的 `MediaQueryData` 数据。
```dart
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
// ACCESSIBILITY
@override
void didChangeAccessibilityFeatures() {
setState(() { });
}
// METRICS
@override
void didChangeMetrics() {
setState(() {});
}
@override
void didChangeTextScaleFactor() {
setState(() { });
}
// RENDERING
@override
void didChangePlatformBrightness() {
setState(() {});
}
@override
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
if (!kReleaseMode) {
data = data.copyWith(platformBrightness: debugBrightnessOverride);
}
return MediaQuery(
data: data,
child: widget.child,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
```
举个例子,如下图所示,从 Android 的 Java 层弹出键盘开始,会把改变后的视图信息传递给 C++ 层,最后回调到 Dart 层,从而触发 `MaterialApp` 内的 `didChangeMetrics` 方法执行 ` setState(() {});` ,进而让 `_MediaQueryFromWindow` 内的 `build` 更新了 `MediaQueryData` ,最终改变了 `Scaffod``body` 大小。
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image12)
那么到这里,你知道如何在 Flutter 里正确地去获取键盘的高度了吧?
### 最后
从一个简单的 `resizeToAvoidBottomInset` 去拓展到 `Scaffod` 的内部布局和 `MediaQueryData` 与键盘的关系,其实这也是学习框架过程中很好的知识延伸,通过特定的问题去深入理解框架的实现原理,最后再把知识点和问题关联起来,这样问题在此之后便不再是问题,因为入脑了~
![](http://img.cdn.guoshuyu.cn/20210429_Flutter-KEY/image13)