【譯】Flutter | 深入理解佈局約束

Vadaski發表於2020-07-06

原文:Flutter: The Advanced Layout Rule Even Beginners Must Know

作者:Marcelo Glasberg

譯者:Vadaski

校對:Luke ChengAlex

【譯】Flutter | 深入理解佈局約束

前言

這篇文章最初來自於 Marcelo Glasberg 在 Medium 發表的 Flutter: The Advanced Layout Rule Even Beginners Must Know。後被 Flutter Team 發現並收錄到 flutter.dev

在認真閱讀完這篇文章後,我認為它對 Flutter 開發者來說具有相當的 指導意義,每一位 Flutter 開發都應該認真理解其中的佈局約束過程,是非常必要的!因此,在翻譯本文的過程中,我們對譯文反覆打磨,儘可能保留原文向傳遞給讀者的內容。希望讓每一位看到此文的開發者都能夠有所收穫。

深入理解佈局約束

我們會經常聽到一些開發者在學習 Flutter 時的疑惑:為什麼我設定了 width:100, 但是看上去卻不是 100 畫素寬呢。(注意,本文中的“畫素”均指的是邏輯畫素) 通常你會回答,將這個 Widget 放進 Center 中,對吧?

別這麼幹。

如果你這樣做了,他們會不斷找你詢問這樣的問題:為什麼 FittedBox 又不起作用了? 為什麼 Column 又溢位邊界,亦或是 IntrinsicWidth 應該做什麼。

其實我們首先應該做的,是告訴他們 Flutter 的佈局方式與 HTML 的佈局差異相當大 (這些開發者很可能是 Web 開發),然後要讓他們熟記這條規則:

  • 首先,上層 widget 向下層 widget 傳遞約束條件。

  • 然後,下層 widget 向上層 widget 傳遞大小資訊。

  • 最後,上層 widget 決定下層 widget 的位置。

如果我們在開發時無法熟練運用這條規則,在佈局時就不能完全理解其原理,所以越早掌握這條規則越好!

更多細節:

  • Widget 會通過它的 父級 獲得自身的約束。 約束實際上就是 4 個浮點型別的集合: 最大/最小寬度,以及最大/最小高度。

  • 然後,這個 widget 將會逐個遍歷它的 children 列表。向子級傳遞 約束(子級之間的約束可能會有所不同),然後詢問它的每一個子級需要用於佈局的大小。

  • 然後,這個 widget 就會對它子級的 children 逐個進行佈局。 (水平方向是 x 軸,豎直是 y 軸)

  • 最後,widget 將會把它的大小資訊向上傳遞至父 widget(包括其原始約束條件)。

例如,如果一個 widget 中包含了一個具有 padding 的 Column, 並且要對 Column 的子 widget 進行如下的佈局:

【譯】Flutter | 深入理解佈局約束

那麼談判將會像這樣:

Widget: "嘿!我的父級。我的約束是多少?"

Parent: "你的寬度必須在 80300 畫素之間,高度必須在 3085 之間。"

Widget: "嗯...我想要 5 個畫素的內邊距,這樣我的子級能最多擁有 290 個畫素寬度和 75 個畫素高度。"

Widget: "嘿,我的第一個子級,你的寬度必須要在 0290,長度在 075 之間。"

First child: "OK,那我想要 290 畫素的寬度,20 個畫素的長度。"

Widget: "嗯...由於我想要將我的第二個子級放在第一個子級下面,所以我們僅剩 55 個畫素的高度給第二個子級了。"

Widget: "嘿,我的第二個子級,你的寬度必須要在 0290,長度在 055 之間。"

Second child: "OK,那我想要 140 畫素的寬度,30 個畫素的長度。"

Widget: "很好。我的第一個子級將被放在 x: 5 & y: 5 的位置, 而我的第二個子級將在 x: 80 & y: 25 的位置。"

Widget: "嘿,我的父級,我決定我的大小為 300 畫素寬度,60 畫素高度。"

限制

正如上述所介紹的佈局規則中所說的那樣, Flutter 的佈局引擎有一些重要限制:

  • 一個 widget 僅在其父級給其約束的情況下才能決定自身的大小。 這意味著 widget 通常情況下 不能任意獲得其想要的大小

  • 一個 widget 無法知道,也不需要決定其在螢幕中的位置。 因為它的位置是由其父級決定的。

  • 當輪到父級決定其大小和位置的時候,同樣的也取決於它自身的父級。 所以,在不考慮整棵樹的情況下,幾乎不可能精確定義任何 widget 的大小和位置。

樣例

下面的示例由 DartPad 提供,具有良好的互動體驗。 使用下面水平滾動條的編號切換 29 個不同的示例。

你可以在 flutter.cn 上找到該原始碼。

如果你願意的話,你還可以在 這個 Github 倉庫中 獲取其程式碼。

以下各節將介紹這些示例。

樣例 1

【譯】Flutter | 深入理解佈局約束

Container(color: Colors.red)
複製程式碼

整個螢幕作為 Container 的父級,並且強制 Container 變成和螢幕一樣的大小。

所以這個 Container 充滿了整個螢幕,並繪製成紅色。

樣例 2

【譯】Flutter | 深入理解佈局約束

Container(width: 100, height: 100, color: Colors.red)
複製程式碼

紅色的 Container 想要變成 100 x 100 的大小, 但是它無法變成,因為螢幕強制它變成和螢幕一樣的大小。

所以 Container 充滿了整個螢幕。

樣例 3

【譯】Flutter | 深入理解佈局約束

Center(
   child: Container(width: 100, height: 100, color: Colors.red)
)
複製程式碼

螢幕強制 Center 變得和螢幕一樣大,所以 Center 充滿了螢幕。

然後 Center 告訴 Container 可以變成任意大小,但是不能超出螢幕。 現在,Container 可以真正變成 100 × 100 大小了。

樣例 4

【譯】Flutter | 深入理解佈局約束

Align(
   alignment: Alignment.bottomRight,
   child: Container(width: 100, height: 100, color: Colors.red),
)
複製程式碼

與上一個樣例不同的是,我們使用了 Align 而不是 Center

Align 同樣也告訴 Container,你可以變成任意大小。 但是,如果還留有空白空間的話,它不會居中 Container。 相反,它將會在允許的空間內,把 Container 放在右下角(bottomRight)。

樣例 5

【譯】Flutter | 深入理解佈局約束

Center(
   child: Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
   )
)
複製程式碼

螢幕強制 Center 變得和螢幕一樣大,所以 Center 充滿螢幕。

然後 Center 告訴 Container 可以變成任意大小,但是不能超出螢幕。 現在,Container 想要無限的大小,但是由於它不能比螢幕更大, 所以就僅充滿螢幕。

樣例 6

【譯】Flutter | 深入理解佈局約束

Center(child: Container(color: Colors.red))
複製程式碼

螢幕強制 Center 變得和螢幕一樣大,所以 Center 充滿螢幕。

然後 Center 告訴 Container 可以變成任意大小,但是不能超出螢幕。 由於 Container 沒有子級而且沒有固定大小,所以它決定能有多大就有多大, 所以它充滿了整個螢幕。

但是,為什麼 Container 做出了這個決定? 非常簡單,因為這個決定是由 Container widget 的建立者決定的。 可能會因創造者而異,而且你還得閱讀 Container 文件 來理解不同場景下它的行為。

樣例 7

【譯】Flutter | 深入理解佈局約束

Center(
   child: Container(
      color: Colors.red,
      child: Container(color: Colors.green, width: 30, height: 30),
   )
)
複製程式碼

螢幕強制 Center 變得和螢幕一樣大,所以 Center 充滿螢幕。

然後 Center 告訴紅色的 Container 可以變成任意大小,但是不能超出螢幕。 由於 Container 沒有固定大小但是有子級,所以它決定變成它 child 的大小。

然後紅色的 Container 告訴它的 child 可以變成任意大小,但是不能超出螢幕。

而它的 child 是一個想要 30 × 30 大小綠色的 Container。由於紅色的 Container 和其子級一樣大,所以也變為 30 × 30。由於綠色的 Container 完全覆蓋了紅色 Container, 所以你看不見它了。

樣例 8

【譯】Flutter | 深入理解佈局約束

Center(
   child: Container(
     color: Colors.red,
     padding: const EdgeInsets.all(20.0),
     child: Container(color: Colors.green, width: 30, height: 30),
   )
)
複製程式碼

紅色 Container 變為其子級的大小,但是它將其 padding 帶入了約束的計算中。 所以它有一個 30 x 30 的外邊距。由於這個外邊距,所以現在你能看見紅色了。 而綠色的 Container 則還是和之前一樣。

樣例 9

【譯】Flutter | 深入理解佈局約束

ConstrainedBox(
   constraints: BoxConstraints(
      minWidth: 70, 
      minHeight: 70,
      maxWidth: 150, 
      maxHeight: 150,
   ),
   child: Container(color: Colors.red, width: 10, height: 10),
)
複製程式碼

你可能會猜想 Container 的尺寸會在 70 到 150 畫素之間,但並不是這樣。 ConstrainedBox 僅對其從其父級接收到的約束下施加其他約束。

在這裡,螢幕迫使 ConstrainedBox 與螢幕大小完全相同, 因此它告訴其子 Widget 也以螢幕大小作為約束, 從而忽略了其 constraints 引數帶來的影響。

樣例 10

【譯】Flutter | 深入理解佈局約束

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 10, height: 10),
   )    
)
複製程式碼

現在,Center 允許 ConstrainedBox 達到螢幕可允許的任意大小。 ConstrainedBoxconstraints 引數帶來的約束附加到其子物件上。

Container 必須介於 70 到 150 畫素之間。雖然它希望自己有 10 個畫素大小, 但最終獲得了 70 個畫素(最小為 70)。

樣例 11

【譯】Flutter | 深入理解佈局約束

Center(
  child: ConstrainedBox(
     constraints: BoxConstraints(
        minWidth: 70, 
        minHeight: 70,
        maxWidth: 150, 
        maxHeight: 150,
        ),
     child: Container(color: Colors.red, width: 1000, height: 1000),
  )  
)
複製程式碼

現在,Center 允許 ConstrainedBox 達到螢幕可允許的任意大小。 ConstrainedBoxconstraints 引數帶來的約束附加到其子物件上。

Container 必須介於 70 到 150 畫素之間。 雖然它希望自己有 1000 個畫素大小, 但最終獲得了 150 個畫素(最大為 150)。

樣例 12

【譯】Flutter | 深入理解佈局約束

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 100, height: 100),
   ) 
)
複製程式碼

現在,Center 允許 ConstrainedBox 達到螢幕可允許的任意大小。 ConstrainedBoxconstraints 引數帶來的約束附加到其子物件上。

Container 必須介於 70 到 150 畫素之間。 雖然它希望自己有 100 個畫素大小, 因為 100 介於 70 至 150 的範圍內,所以最終獲得了 100 個畫素。

樣例 13

【譯】Flutter | 深入理解佈局約束

UnconstrainedBox(
   child: Container(color: Colors.red, width: 20, height: 50),
)
複製程式碼

螢幕強制 UnconstrainedBox 變得和螢幕一樣大,而 UnconstrainedBox 允許其子級的 Container 可以變為任意大小。

樣例 14

【譯】Flutter | 深入理解佈局約束

UnconstrainedBox(
   child: Container(color: Colors.red, width: 4000, height: 50),
)
複製程式碼

螢幕強制 UnconstrainedBox 變得和螢幕一樣大, 而 UnconstrainedBox 允許其子級的 Container 可以變為任意大小。

不幸的是,在這種情況下,容器的寬度為 4000 畫素, 這實在是太大,以至於無法容納在 UnconstrainedBox 中, 因此 UnconstrainedBox 將顯示溢位警告(overflow warning)。

樣例 15

【譯】Flutter | 深入理解佈局約束

OverflowBox(
   minWidth: 0.0,
   minHeight: 0.0,
   maxWidth: double.infinity,
   maxHeight: double.infinity,
   child: Container(color: Colors.red, width: 4000, height: 50),
);
複製程式碼

螢幕強制 OverflowBox 變得和螢幕一樣大, 並且 OverflowBox 允許其子容器設定為任意大小。

OverflowBoxUnconstrainedBox 類似,但不同的是, 如果其子級超出該空間,它將不會顯示任何警告。

在這種情況下,容器的寬度為 4000 畫素,並且太大而無法容納在 OverflowBox 中, 但是 OverflowBox 會全部顯示,而不會發出警告。

樣例 16

【譯】Flutter | 深入理解佈局約束

UnconstrainedBox(
   child: Container(
      color: Colors.red, 
      width: double.infinity, 
      height: 100,
   )
)
複製程式碼

這將不會渲染任何東西,而且你能在控制檯看到錯誤資訊。

UnconstrainedBox 讓它的子級決定成為任何大小, 但是其子級是一個具有無限大小的 Container

Flutter 無法渲染無限大的東西,所以它丟擲以下錯誤: BoxConstraints forces an infinite width.(盒子約束強制使用了無限的寬度)

樣例 17

【譯】Flutter | 深入理解佈局約束

UnconstrainedBox(
   child: LimitedBox(
      maxWidth: 100,
      child: Container( 
         color: Colors.red,
         width: double.infinity, 
         height: 100,
      )
   )
)
複製程式碼

這次你就不會遇到報錯了。 UnconstrainedBoxLimitedBox 一個無限的大小; 但它向其子級傳遞了最大為 100 的約束。

如果你將 UnconstrainedBox 替換為 Center, 則LimitedBox 將不再應用其限制(因為其限制僅在獲得無限約束時才適用), 並且容器的寬度允許超過 100。

上面的樣例解釋了 LimitedBoxConstrainedBox 之間的區別。

樣例 18

【譯】Flutter | 深入理解佈局約束

FittedBox(
   child: Text('Some Example Text.'),
)
複製程式碼

螢幕強制 FittedBox 變得和螢幕一樣大, 而 Text 則是有一個自然寬度(也被稱作 intrinsic 寬度), 它取決於文字數量,字型大小等因素。

FittedBoxText 可以變為任意大小。 但是在 Text 告訴 FittedBox 其大小後, FittedBox 縮放文字直到填滿所有可用寬度。

樣例 19

【譯】Flutter | 深入理解佈局約束

Center(
   child: FittedBox(
      child: Text('Some Example Text.'),
   )
)
複製程式碼

但如果你將 FittedBox 放進 Center widget 中會發生什麼? Center 將會讓 FittedBox 能夠變為任意大小, 取決於螢幕大小。

FittedBox 然後會根據 Text 調整自己的大小, 然後讓 Text 可以變為所需的任意大小, 由於二者具有同一大小,因此不會發生縮放。

樣例 20

【譯】Flutter | 深入理解佈局約束

Center(
   child: FittedBox(
      child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
   )
)
複製程式碼

然而,如果 FittedBox 位於 Center 中, 但 Text 太大而超出螢幕,會發生什麼?

FittedBox 會嘗試根據 Text 大小調整大小, 但不能大於螢幕大小。然後假定螢幕大小, 並調整 Text 的大小以使其也適合螢幕。

樣例 21

【譯】Flutter | 深入理解佈局約束

Center(
   child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
複製程式碼

然而,如果你刪除了 FittedBoxText 則會從螢幕上獲取其最大寬度, 並在合適的地方換行。

樣例 22

【譯】Flutter | 深入理解佈局約束

FittedBox(
   child: Container(
      height: 20.0, 
      width: double.infinity,
   )
)
複製程式碼

FittedBox 只能在有限制的寬高中 對子 widget 進行縮放(寬度和高度不會變得無限大)。 否則,它將無法渲染任何內容,並且你會在控制檯中看到錯誤。

樣例 23

【譯】Flutter | 深入理解佈局約束

Row(
   children:[
      Container(color: Colors.red, child: Text('Hello!')),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)
複製程式碼

螢幕強制 Row 變得和螢幕一樣大,所以 Row 充滿螢幕。

UnconstrainedBox 一樣, Row 也不會對其子代施加任何約束, 而是讓它們成為所需的任意大小。 Row 然後將它們並排放置, 任何多餘的空間都將保持空白。

樣例 24

【譯】Flutter | 深入理解佈局約束

Row(
   children:[
      Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)
複製程式碼

由於 Row 不會對其子級施加任何約束, 因此它的 children 很有可能太大 而超出 Row 的可用寬度。在這種情況下, Row 會和 UnconstrainedBox 一樣顯示溢位警告。

樣例 25

【譯】Flutter | 深入理解佈局約束

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
      ),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)
複製程式碼

Row 的子級被包裹在了 Expanded widget 之後, Row 將不會再讓其決定自身的寬度了。

取而代之的是,Row 會根據所有 Expanded 的子級 來計算其該有的寬度。

換句話說,一旦你使用 Expanded, 子級自身的寬度就變得無關緊要,直接會被忽略掉。

樣例 26

【譯】Flutter | 深入理解佈局約束

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text(‘This is a very long text that won’t fit the line.’)),
      ),
      Expanded(
         child: Container(color: Colors.green, child: Text(‘Goodbye!’),
      ),
   ]
)
複製程式碼

如果所有 Row 的子級都被包裹了 Expanded widget, 每一個 Expanded 大小都會與其 flex 因子成比例, 並且 Expanded widget 將會強制其子級具有與 Expanded 相同的寬度。

換句話說,Expanded 忽略了其子 Widget 想要的寬度。

樣例 27

【譯】Flutter | 深入理解佈局約束

Row(children:[
  Flexible(
    child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
  Flexible(
    child: Container(color: Colors.green, child: Text(‘Goodbye!’))),
  ]
)
複製程式碼

如果你使用 Flexible 而不是 Expanded 的話, 唯一的區別是,Flexible 會讓其子級具有與 Flexible 相同或者更小的寬度。 而 Expanded 將會強制其子級具有和 Expanded 相同的寬度。 但無論是 Expanded 還是 Flexible 在它們決定子級大小時都會忽略其寬度。

這意味著,Row 要麼使用子級的寬度, 要麼使用ExpandedFlexible 從而忽略子級的寬度。

樣例 28

【譯】Flutter | 深入理解佈局約束

Scaffold(
   body: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello!'),
            Text('Goodbye!'),
         ]
      )))
複製程式碼

螢幕強制 Scaffold 變得和螢幕一樣大, 所以 Scaffold 充滿螢幕。 然後 Scaffold 告訴 Container 可以變為任意大小, 但不能超出螢幕。

當一個 widget 告訴其子級可以比自身更小的話, 我們通常稱這個 widget 對其子級使用 寬鬆約束(loose)

樣例 29

【譯】Flutter | 深入理解佈局約束

Scaffold(
body: SizedBox.expand(
   child: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello!'),
            Text('Goodbye!'),
         ],
      ))))
複製程式碼

如果你想要 Scaffold 的子級變得和 Scaffold 本身一樣大的話, 你可以將這個子級外包裹一個 SizedBox.expand

當一個 widget 告訴它的子級必須變成某個大小的時候, 我們通常稱這個 widget 對其子級使用 嚴格約束(tight)

嚴格約束(Tight) vs 寬鬆約束(loose)

以後你經常會聽到一些約束為嚴格約束或寬鬆約束, 你花點時間來弄明白它們是值得的。

嚴格約束給你了一種獲得確切大小的選擇。 換句話來說就是,它的最大/最小寬度是一致的,高度也一樣。

如果你到 Flutter 的 box.dart 檔案中搜尋 BoxConstraints 構造器,你會發現以下內容:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;
複製程式碼

如果你重新閱讀 樣例 2, 它告訴我們螢幕強制 Container 變得和螢幕一樣大。 為何螢幕能夠做到這一點, 原因就是給 Container 傳遞了嚴格約束。

一個寬鬆約束換句話來說就是設定了最大寬度/高度, 但是讓允許其子 widget 獲得比它更小的任意大小。 換句話來說,寬鬆約束的最小寬度/高度為 0

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;
複製程式碼

如果你訪問 樣例 3, 它將會告訴我們 Center 讓紅色的 Container 變得更小, 但是不能超出螢幕。Center 能夠做到這一點的原因就在於 給 Container 的是一個寬鬆約束。 總的來說,Center 起的作用就是從其父級(螢幕)那裡獲得的嚴格約束, 為其子級(Container)轉換為寬鬆約束。

瞭解如何為特定 widget 制定佈局規則

掌握通用佈局是非常重要的,但這還不夠。

應用一般規則時,每個 widget 都具有很大的自由度, 所以沒有辦法只看 widget 的名稱就知道可能它長什麼樣。

如果你嘗試推測,可能就會猜錯。 除非你已閱讀 widget 的文件或研究了其原始碼, 否則你無法確切知道 widget 的行為。

佈局原始碼通常很複雜,因此閱讀文件是更好的選擇。 但是當你在研究佈局原始碼時,可以使用 IDE 的導航功能輕鬆找到它。

下面是一個例子:

  • 在你的程式碼中找到一個 Column 並跟進到它的原始碼。 為此,請在 (Android Studio/IntelliJ) 中使用 command+B(macOS)或 control+B(Windows/Linux)。 你將跳到 basic.dart 檔案中。由於 Column 擴充套件了 Flex, 請導航至 Flex 原始碼(也位於 basic.dart 中)。

  • 向下滾動直到找到一個名為 createRenderObject() 的方法。 如你所見,此方法返回一個 RenderFlex。 它是 Column 的渲染物件, 現在導航到 flex.dart 檔案中的 RenderFlex 的原始碼。

  • 向下滾動,直到找到 performLayout() 方法, 由該方法執行列布局。

【譯】Flutter | 深入理解佈局約束


最後,十分感謝參與校對的程路Alex,以及幫助打磨譯文的 CaiJingLong任宇傑孫愷 以上幾位同學,謝謝!

希望看完這篇文章,能夠對你有所收穫。如果你遇到任何疑惑,或者想要與我討論,歡迎在底部評論區一起交流,或是通過郵箱與我聯絡。Happy coding!

相關文章