原文:Flutter: The Advanced Layout Rule Even Beginners Must Know
譯者:Vadaski
校對:Luke Cheng、Alex
前言
這篇文章最初來自於 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 進行如下的佈局:
那麼談判將會像這樣:
Widget: "嘿!我的父級。我的約束是多少?"
Parent: "你的寬度必須在 80
到 300
畫素之間,高度必須在 30
到 85
之間。"
Widget: "嗯...我想要 5
個畫素的內邊距,這樣我的子級能最多擁有 290
個畫素寬度和 75
個畫素高度。"
Widget: "嘿,我的第一個子級,你的寬度必須要在 0
到 290
,高度在 0
到 75
之間。"
First child: "OK,那我想要 290
畫素的寬度,20
個畫素的高度。"
Widget: "嗯...由於我想要將我的第二個子級放在第一個子級下面,所以我們僅剩 55
個畫素的高度給第二個子級了。"
Widget: "嘿,我的第二個子級,你的寬度必須要在 0
到 290
,高度在 0
到 55
之間。"
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
達到螢幕可允許的任意大小。
ConstrainedBox
將 constraints
引數帶來的約束附加到其子物件上。
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
達到螢幕可允許的任意大小。
ConstrainedBox
將 constraints
引數帶來的約束附加到其子物件上。
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
達到螢幕可允許的任意大小。
ConstrainedBox
將 constraints
引數帶來的約束附加到其子物件上。
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
允許其子容器設定為任意大小。
OverflowBox
與 UnconstrainedBox
類似,但不同的是,
如果其子級超出該空間,它將不會顯示任何警告。
在這種情況下,容器的寬度為 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,
)
)
)
複製程式碼
這次你就不會遇到報錯了。
UnconstrainedBox
給 LimitedBox
一個無限的大小;
但它向其子級傳遞了最大為 100 的約束。
如果你將 UnconstrainedBox
替換為 Center
,
則LimitedBox
將不再應用其限制(因為其限制僅在獲得無限約束時才適用),
並且容器的寬度允許超過 100。
上面的樣例解釋了 LimitedBox
和 ConstrainedBox
之間的區別。
樣例 18
FittedBox(
child: Text('Some Example Text.'),
)
複製程式碼
螢幕強制 FittedBox
變得和螢幕一樣大,
而 Text
則是有一個自然寬度(也被稱作 intrinsic 寬度),
它取決於文字數量,字型大小等因素。
FittedBox
讓 Text
可以變為任意大小。
但是在 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.'),
)
複製程式碼
然而,如果你刪除了 FittedBox
,
Text
則會從螢幕上獲取其最大寬度,
並在合適的地方換行。
樣例 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
要麼使用子級的寬度, 要麼使用Expanded
和Flexible
從而忽略子級的寬度。
樣例 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!