Flutter 中鍵盤彈起時肃弟,Scaffold 發(fā)生了什么變化

最近剛好有網(wǎng)友咨詢一個問題,那就順便借著這個問題給大家深入介紹下 Flutter 中鍵盤彈起時穷缤,Scaffold 的內(nèi)部發(fā)生了什么變化箩兽,讓大家更好理解 Flutter 中的輸入鍵盤和 Scaffold 的關(guān)系。

如下圖所示汗贫,當(dāng)時的問題是:當(dāng)界面內(nèi)有 TextField 輸入框時,點擊鍵盤彈起后部蛇,界面內(nèi)底部的按鍵和 FloatButton 會被擠到鍵盤上面咐蝇,有什么辦法可以讓底部按鍵和 FloatButton 不被頂上來嗎?

image

其實解決這個問題很簡單撮竿,那就是只要ScaffoldresizeToAvoidBottomInset 配置為 false 笔呀,結(jié)果如下圖所示,鍵盤彈起后底部按鍵和 FloatButton 不會再被頂上來房蝉,問題解決微渠。那為什么鍵盤彈起會和 resizeToAvoidBottomInset 有關(guān)系?

image

Scaffold 的 resize

Scaffold 是 Flutter 中最常用的頁面腳手架檀蹋,前面知道了通過 resizeToAvoidBottomInset 云芦,我們可以配置在鍵盤彈起時頁面的底部按鍵和 FloatButton 不會再被頂上來贸桶,其實這個行為是因為 Scaffoldbody 大小被 resize 了皇筛。

那這個過程是怎么發(fā)生的呢坠七?首先如下圖所示,我們在 Scaffold 的源碼里可以看到拄踪,當(dāng)resizeToAvoidBottomInset 為 true 時拳魁,會使用 mediaQuery.viewInsets.bottom 作為 minInsets 的參數(shù),也就是可以確定:鍵盤彈起時的界面 resizemediaQuery.viewInsets.bottom 有關(guān)系耀盗。

image

而如下圖所示卦尊, Scaffold 內(nèi)部的布局主要是靠 CustomMultiChildLayoutCustomMultiChildLayout 的布局邏輯主要在 MultiChildLayoutDelegate 對象里忿薇。

前面獲取到的 minInsets 會被用到 _ScaffoldLayout 這個 MultiChildLayoutDelegate 里面躏哩,也就是說 Scaffold 的內(nèi)部是通過 CustomMultiChildLayout 實現(xiàn)的布局,具體實現(xiàn)邏輯在 _ScaffoldLayout 這個 Delegate筋栋。

image

關(guān)于 CustomMultiChildLayout 的詳細使用介紹在之前的文章 《詳解自定義布局實戰(zhàn)》 里可以找到正驻。

接著看 _ScaffoldLayout , 在 _ScaffoldLayout 進行布局時襟交,會通過傳入的
minInsets 來決定 body 顯示的 contentBottom 捣域, 所以可以看到事實上傳入的 minInsets 改變的是 Scaffold 布局的 bottom 位置

image

上圖代碼中使用的 _ScaffoldSlot.body 這個枚舉其實是作為 LayoutId 的值迹鹅,MultiChildLayoutDelegate 在布局時可以通過 LayoutId 獲取到對應(yīng) child 進行布局操作丘侠,詳細可見: 《詳解自定義布局實戰(zhàn)》

image

那么 Scaffoldbody 是什么呢逐样? 如上圖代碼所示,其實 Scaffoldbody 是一個叫 _BodyBuilder 的對象脂新,而這個 _BodyBuilder 內(nèi)部其實是一個 LayoutBuilder。(注意级零,在 widget.appbar 不為 null 時滞乙,會 removeTopPadding

所以如下圖代碼所示 body 在添加時,它父級的MediaQueryData 會被重載序调,特別是 removeTopPadding 會被清空兔簇,viewInsets.bottom 也是會被重置

image

最后如下代碼所示边酒,_BodyBuilderLayoutBuilder 里會獲取到一個 topbottom 的參數(shù)狸窘,這兩個參數(shù)都通過前面在 _ScaffoldLayout 布局時傳入的 constraints 去判斷得到翻擒,最終 copyWith 得到新的 MediaQuery

image

這里就涉及到一個有意思的點春哨,在 _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();
  }
}

如上代碼所示:

  • 代碼中定義了 MainWidgetCustomWidget 兩個控件便瑟;
  • MainWidget 里使用了 Scaffold ,并且 CustomWidgetMainWidget 里被使用脊框;
  • 分別在這兩個 Widget 的build 方法里打印出對應(yīng)的 MediaQuery.of(context).paddingMediaQuery.of(context).viewInsets.bottom 的值浇雹;

如下圖所示昭灵,在鍵盤彈起和不彈起時可以看到 padding 值是不同的伐谈,而 viewInsets.bottom 都為 0。

image

為什么 padding 值的 top 會不一致抠蚣,自然是因為 CustomWidgetMainWidget獲取到的 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)被重載清除了近上。
image

總結(jié)一下:ScaffoldresizeToAvoidBottomInset 會通過 MediaQueryData 影響 body 的布局剔宪,同時在 Scaffold 內(nèi) MediaQuery 會被重載,所以使用的 context 位置不同壹无,獲取到的 MediaQueryData 也不同葱绒,如果需要獲取鍵盤高度和狀態(tài)欄高度的話哈街,最好使用 Scaffold 外的 context

image

這里講了 MediaQueryMediaQueryData 的內(nèi)容,為什么 MediaQuery 通過嵌套就可以重載?為什么通過 context 可以往上獲取到離 context 最近的 MediaQueryData?因為 MediaQuery 是一個 InheritedWidget : 《全面理解State》

鍵盤如何影響 Scaffold

前面我們聊了 ScaffoldresizeToAvoidBottomInset 會通過 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 催束,最終改變了 Scaffodbody 大小速妖。

image

那么到這里露泊,你知道如何在 Flutter 里正確地去獲取鍵盤的高度了吧脖咐?

最后

從一個簡單的 resizeToAvoidBottomInset 去拓展到 Scaffod 的內(nèi)部布局和 MediaQueryData 與鍵盤的關(guān)系,其實這也是學(xué)習(xí)框架過程中很好的知識延伸胶果,通過特定的問題去深入理解框架的實現(xiàn)原理,最后再把知識點和問題關(guān)聯(lián)起來,這樣問題在此之后便不再是問題,因為入腦了~

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末囚聚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瓶蝴,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異畏纲,居然都是意外死亡,警方通過查閱死者的電腦和手機票灰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雕薪,你說我怎么就攤上這事∷” “怎么了稚配?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵臊岸,是天一觀的道長。 經(jīng)常有香客問我奠旺,道長践盼,這世上最難降的妖魔是什么渔伯? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任蓝厌,我火速辦了婚禮疹吃,結(jié)果婚禮上育谬,老公的妹妹穿的比我還像新娘帮哈。我一直安慰自己膛檀,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布娘侍。 她就那樣靜靜地躺著咖刃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪憾筏。 梳的紋絲不亂的頭發(fā)上嚎杨,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機與錄音氧腰,去河邊找鬼枫浙。 笑死,一個胖子當(dāng)著我的面吹牛古拴,可吹牛的內(nèi)容都是我干的箩帚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼黄痪,長吁一口氣:“原來是場噩夢啊……” “哼紧帕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起桅打,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤焕参,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后油额,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡刻帚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年潦嘶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崇众。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡掂僵,死狀恐怖航厚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锰蓬,我是刑警寧澤幔睬,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站芹扭,受9級特大地震影響麻顶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜舱卡,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一辅肾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧轮锥,春花似錦矫钓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至既绩,卻和暖如春概龄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背熬词。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工旁钧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人互拾。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓歪今,卻偏偏與公主長得像,于是被迫代替她去往敵國和親颜矿。 傳聞我的和親對象是個殘疾皇子寄猩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容