注册

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

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


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


7ad55e7e3cbba80e7de945d8866537d5.gif


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


53f5e3ababdd1d5c15b807d005634df4.gif


Scaffold 的 resize

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


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


18a3a99e48bcd2c2d6902d04ddafa077.png


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


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


d4d9e602e021b69c1aa4c6bb883139ce.png


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



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


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


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


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


ac6ec71c0c7e63971a1104f29baa2fb6.png


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


f3be1d8678f3f08e0d7a81458209ba03.png

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


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。


4c3c99c2bef9f546e5f7aaf4c67311df.png


为什么 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 内被重载清除了。


11b8208ef0d764b9aadc9b89c91c78d4.png

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


59b704863de8076a15bb1fa7e57cbfc7.png
> 这里讲了 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 数据。
` @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 大小。


c911607c0a8b682eef2a7c30b1e9312c.png


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


最后

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


fc147edf897676e804191b60421d5c69.png

转自:https://www.agora.io/cn/community/blog/121-category/21060

0 个评论

要回复文章请先登录注册