最近剛好有網(wǎng)友咨詢一個問題,那就順便借著這個問題給大家深入介紹下 Flutter 中鍵盤彈起時穷缤,Scaffold
的內(nèi)部發(fā)生了什么變化箩兽,讓大家更好理解 Flutter 中的輸入鍵盤和 Scaffold
的關(guān)系。
如下圖所示汗贫,當(dāng)時的問題是:當(dāng)界面內(nèi)有 TextField
輸入框時,點擊鍵盤彈起后部蛇,界面內(nèi)底部的按鍵和 FloatButton 會被擠到鍵盤上面咐蝇,有什么辦法可以讓底部按鍵和 FloatButton 不被頂上來嗎?
其實解決這個問題很簡單撮竿,那就是只要把 Scaffold
的 resizeToAvoidBottomInset
配置為 false
笔呀,結(jié)果如下圖所示,鍵盤彈起后底部按鍵和 FloatButton 不會再被頂上來房蝉,問題解決微渠。那為什么鍵盤彈起會和 resizeToAvoidBottomInset
有關(guān)系?
Scaffold 的 resize
Scaffold
是 Flutter 中最常用的頁面腳手架檀蹋,前面知道了通過 resizeToAvoidBottomInset
云芦,我們可以配置在鍵盤彈起時頁面的底部按鍵和 FloatButton 不會再被頂上來贸桶,其實這個行為是因為 Scaffold
的 body
大小被 resize
了皇筛。
那這個過程是怎么發(fā)生的呢坠七?首先如下圖所示,我們在 Scaffold
的源碼里可以看到拄踪,當(dāng)resizeToAvoidBottomInset
為 true 時拳魁,會使用 mediaQuery.viewInsets.bottom
作為 minInsets
的參數(shù),也就是可以確定:鍵盤彈起時的界面 resize
和 mediaQuery.viewInsets.bottom
有關(guān)系耀盗。
而如下圖所示卦尊, Scaffold
內(nèi)部的布局主要是靠 CustomMultiChildLayout
,CustomMultiChildLayout
的布局邏輯主要在 MultiChildLayoutDelegate
對象里忿薇。
前面獲取到的 minInsets
會被用到 _ScaffoldLayout
這個 MultiChildLayoutDelegate
里面躏哩,也就是說 Scaffold
的內(nèi)部是通過 CustomMultiChildLayout
實現(xiàn)的布局,具體實現(xiàn)邏輯在 _ScaffoldLayout
這個 Delegate
里筋栋。
關(guān)于
CustomMultiChildLayout
的詳細使用介紹在之前的文章 《詳解自定義布局實戰(zhàn)》 里可以找到正驻。
接著看 _ScaffoldLayout
, 在 _ScaffoldLayout
進行布局時襟交,會通過傳入的
minInsets
來決定 body
顯示的 contentBottom
捣域, 所以可以看到事實上傳入的 minInsets
改變的是 Scaffold
布局的 bottom 位置。
上圖代碼中使用的
_ScaffoldSlot.body
這個枚舉其實是作為LayoutId
的值迹鹅,MultiChildLayoutDelegate
在布局時可以通過LayoutId
獲取到對應(yīng) child 進行布局操作丘侠,詳細可見: 《詳解自定義布局實戰(zhàn)》
那么 Scaffold
的 body
是什么呢逐样? 如上圖代碼所示,其實 Scaffold
的 body
是一個叫 _BodyBuilder
的對象脂新,而這個 _BodyBuilder
內(nèi)部其實是一個 LayoutBuilder
。(注意级零,在 widget.appbar
不為 null
時滞乙,會 removeTopPadding
)
所以如下圖代碼所示 body
在添加時,它父級的MediaQueryData
會被重載序调,特別是 removeTopPadding
會被清空兔簇,viewInsets.bottom
也是會被重置。
最后如下代碼所示边酒,_BodyBuilder
的 LayoutBuilder
里會獲取到一個 top
和 bottom
的參數(shù)狸窘,這兩個參數(shù)都通過前面在 _ScaffoldLayout
布局時傳入的 constraints
去判斷得到翻擒,最終 copyWith
得到新的 MediaQuery
。
這里就涉及到一個有意思的點春哨,在 _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
方法里打印出對應(yīng)的MediaQuery.of(context).padding
和MediaQuery.of(context).viewInsets.bottom
的值浇雹;
如下圖所示昭灵,在鍵盤彈起和不彈起時可以看到 padding
值是不同的伐谈,而 viewInsets.bottom
都為 0。
為什么 padding
值的 top
會不一致抠蚣,自然是因為 CustomWidget
和 MainWidget
獲取到的 MediaQuery.of(context)
對象不是同一個數(shù)據(jù)非春。
-
MainWidget
使用的MediaQuery.of(context)
得到的MediaQueryData
是上級往下傳遞的,里面包含了top:47
的狀態(tài)欄高度和bottom:34
的底部安全區(qū)域高度护侮。
-
CustomWidget
里面MediaQuery.of(context)
得到的MediaQueryData
储耐,自然就是前面分析過的_BodyBuilder
里的通過copyWith
得到新的MediaQuery
,所以CustomWidget
得到的MediaQueryData
其實在Scaffold
內(nèi)部已經(jīng)被重置了什湘,所以它的top:0
长赞,獲取不到狀態(tài)欄高度。
事實上這就是大家為什么有時候
MediaQuery.of( context)
可以獲取到狀態(tài)欄高度闽撤,有時候又獲取不到的原因得哆,因為你的context
獲取到的是Scaffold
之外的MediaQueryData
, 還是Scaffold
內(nèi)被重載過的MediaQueryData
哟旗,自然會得到不一樣的結(jié)果贩据。
如下圖所示栋操,鍵盤彈起因為被 resize 了,所以界面的 bottom
安全區(qū)域變成了 0 饱亮,而
- 在
MainWidget
中可以獲取到viewInsets.bottom
也就是鍵盤的高度矾芙; - 在
CustomWidget
獲取不到viewInsets.bottom
,因為在Scaffold
內(nèi)被重載清除了近上。
總結(jié)一下:Scaffold
的 resizeToAvoidBottomInset
會通過 MediaQueryData
影響 body 的布局剔宪,同時在 Scaffold
內(nèi) MediaQuery
會被重載,所以使用的 context
位置不同壹无,獲取到的 MediaQueryData
也不同葱绒,如果需要獲取鍵盤高度和狀態(tài)欄高度的話哈街,最好使用 Scaffold
外的 context
。
這里講了
MediaQuery
和MediaQueryData
的內(nèi)容,為什么MediaQuery
通過嵌套就可以重載?為什么通過context
可以往上獲取到離context
最近的MediaQueryData
?因為MediaQuery
是一個InheritedWidget
: 《全面理解State》 。
鍵盤如何影響 Scaffold
前面我們聊了 Scaffold
的 resizeToAvoidBottomInset
會通過 MediaQueryData
影響 body 的布局,那是怎么影響的呢?
事實上這得從 MaterialApp
說起摆舟,在 MaterialApp
內(nèi)部的深處嵌套著一個叫 _MediaQueryFromWindow
的 Widget 驶悟,它在內(nèi)部通過 WidgetsBinding.instance.addObserver
對 App 的各種系統(tǒng)事件做了監(jiān)聽,并且對應(yīng)都執(zhí)行了 setState
诗赌。
所以如下源碼所示递览,當(dāng)鍵盤彈出時儿捧, build
方法會被執(zhí)行亿汞, 而 MediaQueryData
就會通過MediaQueryData.fromWindow
獲取到新的 MediaQueryData
數(shù)據(jù)。
@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++ 層氏豌,最后回調(diào)到 Dart 層碌嘀,從而觸發(fā) MaterialApp
內(nèi)的 didChangeMetrics
方法執(zhí)行 setState(() {});
和蚪,進而讓 _MediaQueryFromWindow
內(nèi)的 build
更新了 MediaQueryData
催束,最終改變了 Scaffod
的 body
大小速妖。
那么到這里露泊,你知道如何在 Flutter 里正確地去獲取鍵盤的高度了吧脖咐?
最后
從一個簡單的 resizeToAvoidBottomInset
去拓展到 Scaffod
的內(nèi)部布局和 MediaQueryData
與鍵盤的關(guān)系,其實這也是學(xué)習(xí)框架過程中很好的知識延伸胶果,通過特定的問題去深入理解框架的實現(xiàn)原理,最后再把知識點和問題關(guān)聯(lián)起來,這樣問題在此之后便不再是問題,因為入腦了~