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

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

做者:Marcelo Glasberggit

譯者:Vadaskigithub

校對:Luke ChengAlexapi

前言

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

在認真閱讀完這篇文章後,我認爲它對 Flutter 開發者來講具備至關的 指導意義,每一位 Flutter 開發都應該認真理解其中的佈局約束過程,是很是必要的!所以,在翻譯本文的過程當中,咱們對譯文反覆打磨,儘量保留原文向傳遞給讀者的內容。但願讓每一位看到此文的開發者都可以有所收穫。佈局

深刻理解佈局約束

咱們會常常聽到一些開發者在學習 Flutter 時的疑惑:爲何我設置了 width:100, 可是看上去卻不是 100 像素寬呢。(注意,本文中的「像素」均指的是邏輯像素) 一般你會回答,將這個 Widget 放進 Center 中,對吧?學習

別這麼幹。字體

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

其實咱們首先應該作的,是告訴他們 Flutter 的佈局方式與 HTML 的佈局差別至關大 (這些開發者極可能是 Web 開發),而後要讓他們熟記這條規則:ui

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

  • 而後,下層 widget 向上層 widget 傳遞大小信息。

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

若是咱們在開發時沒法熟練運用這條規則,在佈局時就不能徹底理解其原理,因此越早掌握這條規則越好!

更多細節:

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

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

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

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

例如,若是一個 widget 中包含了一個具備 padding 的 Column, 而且要對 Column 的子 widget 進行以下的佈局:

那麼談判將會像這樣:

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

Container(color: Colors.red)
複製代碼

整個屏幕做爲 Container 的父級,而且強制 Container 變成和屏幕同樣的大小。

因此這個 Container 充滿了整個屏幕,並繪製成紅色。

樣例 2

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

紅色的 Container 想要變成 100 x 100 的大小, 可是它沒法變成,由於屏幕強制它變成和屏幕同樣的大小。

因此 Container 充滿了整個屏幕。

樣例 3

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

屏幕強制 Center 變得和屏幕同樣大,因此 Center 充滿了屏幕。

而後 Center 告訴 Container 能夠變成任意大小,可是不能超出屏幕。 如今,Container 能夠真正變成 100 × 100 大小了。

樣例 4

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

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

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

樣例 5

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

屏幕強制 Center 變得和屏幕同樣大,因此 Center 充滿屏幕。

而後 Center 告訴 Container 能夠變成任意大小,可是不能超出屏幕。 如今,Container 想要無限的大小,可是因爲它不能比屏幕更大, 因此就僅充滿屏幕。

樣例 6

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

屏幕強制 Center 變得和屏幕同樣大,因此 Center 充滿屏幕。

而後 Center 告訴 Container 能夠變成任意大小,可是不能超出屏幕。 因爲 Container 沒有子級並且沒有固定大小,因此它決定能有多大就有多大, 因此它充滿了整個屏幕。

可是,爲何 Container 作出了這個決定? 很是簡單,由於這個決定是由 Container widget 的建立者決定的。 可能會因創造者而異,並且你還得閱讀 Container 文檔 來理解不一樣場景下它的行爲。

樣例 7

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

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

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

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

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

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

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

屏幕強制 UnconstrainedBox 變得和屏幕同樣大,而 UnconstrainedBox 容許其子級的 Container 能夠變爲任意大小。

樣例 14

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

屏幕強制 UnconstrainedBox 變得和屏幕同樣大, 而 UnconstrainedBox 容許其子級的 Container 能夠變爲任意大小。

不幸的是,在這種狀況下,容器的寬度爲 4000 像素, 這實在是太大,以致於沒法容納在 UnconstrainedBox 中, 所以 UnconstrainedBox 將顯示溢出警告(overflow warning)。

樣例 15

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

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

這將不會渲染任何東西,並且你能在控制檯看到錯誤信息。

UnconstrainedBox 讓它的子級決定成爲任何大小, 可是其子級是一個具備無限大小的 Container

Flutter 沒法渲染無限大的東西,因此它拋出如下錯誤: BoxConstraints forces an infinite width.(盒子約束強制使用了無限的寬度)

樣例 17

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

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

若是你將 UnconstrainedBox 替換爲 Center, 則LimitedBox 將再也不應用其限制(由於其限制僅在得到無限約束時才適用), 而且容器的寬度容許超過 100。

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

樣例 18

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

屏幕強制 FittedBox 變得和屏幕同樣大, 而 Text 則是有一個天然寬度(也被稱做 intrinsic 寬度), 它取決於文本數量,字體大小等因素。

FittedBoxText 能夠變爲任意大小。 可是在 Text 告訴 FittedBox 其大小後, FittedBox 縮放文本直到填滿全部可用寬度。

樣例 19

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

但若是你將 FittedBox 放進 Center widget 中會發生什麼? Center 將會讓 FittedBox 可以變爲任意大小, 取決於屏幕大小。

FittedBox 而後會根據 Text 調整本身的大小, 而後讓 Text 能夠變爲所需的任意大小, 因爲兩者具備同一大小,所以不會發生縮放。

樣例 20

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

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

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

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

樣例 23

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

屏幕強制 Row 變得和屏幕同樣大,因此 Row 充滿屏幕。

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

樣例 24

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

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

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

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

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

屏幕強制 Scaffold 變得和屏幕同樣大, 因此 Scaffold 充滿屏幕。 而後 Scaffold 告訴 Container 能夠變爲任意大小, 但不能超出屏幕。

當一個 widget 告訴其子級能夠比自身更小的話, 咱們一般稱這個 widget 對其子級使用 寬鬆約束(loose)

樣例 29

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() 方法, 由該方法執行列布局。


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

但願看完這篇文章,可以對你有所收穫。若是你遇到任何疑惑,或者想要與我討論,歡迎在底部評論區一塊兒交流,或是經過郵箱與我聯繫。Happy coding!

中文連接:debug.flutter.cn/docs/develo…

相關文章
相關標籤/搜索