GSYFlutterBook/Flutter-KEY.md

228 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

最近刚好有网友咨询一个问题,那就顺便借着这个问题给大家深入介绍下 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)