本文最初來自於社區成員 Marcelo Glasberg 在 Medium 發表的文章——"初學者應知應會的 Flutter 高級布局規則 (Flutter: The Advanced Layout Rule Even Beginners Must Know)",然後被 Flutter Team 發現並收錄到 Flutter 官方文檔,後經中文社區成員翻譯發布至 Flutter 中文文檔網站,如果您對本文有任何建議和討論,請點擊閱讀原文到 GitHub Issue 中反饋。
譯者註:在認真閱讀完這篇文章後,我認為它對 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 進行如下的布局:
那麼談判將會像這樣:
你的寬度必須在 80 到 300 像素之間,高度必須在 30 到 85 之間。
嗯...我想要 5 個像素的內邊距,這樣我的子級能最多擁有 290 個像素寬度和 75 個像素高度。
嘿,我的第一個子級,你的寬度必須要在 0 到 290,長度在 0 到 75 之間。
@First child
OK,那我想要 290 像素的寬度,20 個像素的長度。
嗯...由於我想要將我的第二個子級放在第一個子級下面,所以我們僅剩 55 個像素的高度給第二個子級了。
嘿,我的第二個子級,你的寬度必須要在 0 到 290,長度在 0 到 55 之間。
@Second child
OK,那我想要 140 像素的寬度,30 個像素的長度。
很好。我的第一個子級將被放在 x: 5 & y: 5 的位置, 而我的第二個子級將在 x: 80 & y: 25 的位置。
嘿,我的父級,我決定我的大小為 300 像素寬度,60 像素高度。
@Parent
限制正如上述所介紹的布局規則中所說的那樣,Flutter 的布局引擎有一些重要限制:
一個 widget 僅在其父級給其約束的情況下才能決定自身的大小。這意味著 widget 通常情況下 不能任意獲得其想要的大小。
一個 widget 無法知道,也不需要決定其在屏幕中的位置。因為它的位置是由其父級決定的。
當輪到父級決定其大小和位置的時候,同樣的也取決於它自身的父級。所以,在不考慮整棵樹的情況下,幾乎不可能精確定義任何 widget 的大小和位置。
樣例以下的效果圖片和樣例代碼均可通過下面這個 DartPad 連結查看:dartpad.cn/759e9e061a6d4c247700429ddda09b8b
也可以在這個 GitHub 倉庫中獲得:github.com/marcglasberg/flutter_layout_article
以下將依次介紹這些示例:
樣例 1Container(color: Colors.red)整個屏幕作為 Container 的父級,並且強制 Container 變成和屏幕一樣的大小。
所以這個 Container 充滿了整個屏幕,並繪製成紅色。
樣例 2Container(width: 100, height: 100, color: Colors.red)紅色的 Container 想要變成 100 x 100 的大小,但是它無法變成,因為屏幕強制它變成和屏幕一樣的大小。
所以 Container 充滿了整個屏幕。
樣例 3Center(
child: Container(width: 100, height: 100, color: Colors.red)
)屏幕強制 Center 變得和屏幕一樣大,所以 Center 充滿了屏幕。
然後 Center 告訴 Container 可以變成任意大小,但是不能超出屏幕。現在,Container 可以真正變成 100 × 100 大小了。
樣例 4Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)與上一個樣例不同的是,我們使用了 Align 而不是 Center。
Align 同樣也告訴 Container,你可以變成任意大小。但是,如果還留有空白空間的話,它不會居中 Container。相反,它將會在允許的空間內,把 Container 放在右下角(bottomRight)。
樣例 5Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)屏幕強制 Center 變得和屏幕一樣大,所以 Center 充滿屏幕。
然後 Center 告訴 Container 可以變成任意大小,但是不能超出屏幕。現在,Container 想要無限的大小,但是由於它不能比屏幕更大,所以就僅充滿屏幕。
樣例 6Center(child: Container(color: Colors.red))屏幕強制 Center 變得和屏幕一樣大,所以 Center 充滿屏幕。
然後 Center 告訴 Container 可以變成任意大小,但是不能超出屏幕。由於 Container 沒有子級而且沒有固定大小,所以它決定能有多大就有多大,所以它充滿了整個屏幕。
但是,為什麼 Container 做出了這個決定?非常簡單,因為這個決定是由 Container widget 的創建者決定的。可能會因創造者而異,而且你還得閱讀Container 文檔來理解不同場景下它的行為。
樣例 7Center(
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,所以你看不見它了。
樣例 8Center(
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 則還是和之前一樣。
樣例 9ConstrainedBox(
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 參數帶來的影響。
樣例 10Center(
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)。
樣例 11Center(
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)。
樣例 12Center(
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 個像素。
樣例 13UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)屏幕強制 UnconstrainedBox 變得和屏幕一樣大,而 UnconstrainedBox 允許其子級的 Container 可以變為任意大小。
樣例 14UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)屏幕強制 UnconstrainedBox 變得和屏幕一樣大,而 UnconstrainedBox 允許其子級的 Container 可以變為任意大小。
不幸的是,在這種情況下,容器的寬度為 4000 像素,這實在是太大,以至於無法容納在 UnconstrainedBox 中,因此 UnconstrainedBox 將顯示溢出警告(overflow warning)。
樣例 15OverflowBox(
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 會全部顯示,而不會發出警告。
樣例 16UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)這將不會渲染任何東西,而且你能在控制臺看到錯誤信息。
UnconstrainedBox 讓它的子級決定成為任何大小,但是其子級是一個具有無限大小的 Container。
Flutter 無法渲染無限大的東西,所以它拋出以下錯誤:BoxConstraints forces an infinite width.(盒子約束強制使用了無限的寬度)
樣例 17UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)這次你就不會遇到報錯了。UnconstrainedBox 給 LimitedBox 一個無限的大小;但它向其子級傳遞了最大為 100 的約束。
如果你將 UnconstrainedBox 替換為 Center,則LimitedBox 將不再應用其限制(因為其限制僅在獲得無限約束時才適用),並且容器的寬度允許超過 100。
上面的樣例解釋了 LimitedBox 和 ConstrainedBox 之間的區別。
樣例 18FittedBox(
child: Text('Some Example Text.'),
)屏幕強制 FittedBox 變得和屏幕一樣大,而 Text 則是有一個自然寬度(也被稱作 intrinsic 寬度),它取決於文本數量,字體大小等因素。
FittedBox 讓 Text 可以變為任意大小。但是在 Text 告訴 FittedBox 其大小後,FittedBox 縮放文本直到填滿所有可用寬度。
樣例 19Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)但如果你將 FittedBox 放進 Center widget 中會發生什麼?Center 將會讓 FittedBox 能夠變為任意大小,取決於屏幕大小。
FittedBox 然後會根據 Text 調整自己的大小,然後讓 Text 可以變為所需的任意大小,由於二者具有同一大小,因此不會發生縮放。
樣例 20Center(
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 的大小以使其也適合屏幕。
樣例 21Center(
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 則會從屏幕上獲取其最大寬度,並在合適的地方換行。
樣例 22FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)FittedBox 只能在有限制的寬高中對子 widget 進行縮放(寬度和高度不會變得無限大)。否則,它將無法渲染任何內容,並且你會在控制臺中看到錯誤。
樣例 23Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)屏幕強制 Row 變得和屏幕一樣大,所以 Row 充滿屏幕。
和 UnconstrainedBox 一樣,Row 也不會對其子代施加任何約束,而是讓它們成為所需的任意大小。Row 然後將它們並排放置,任何多餘的空間都將保持空白。
樣例 24Row(
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 一樣顯示溢出警告。
樣例 25Row(
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,子級自身的寬度就變得無關緊要,直接會被忽略掉。
樣例 26Row(
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 想要的寬度。
樣例 27Row(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 從而忽略子級的寬度。
樣例 28Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))屏幕強制 Scaffold 變得和屏幕一樣大,所以 Scaffold 充滿屏幕。然後 Scaffold 告訴 Container 可以變為任意大小,但不能超出屏幕。
當一個 widget 告訴其子級可以比自身更小的話,我們通常稱這個 widget 對其子級使用 寬鬆約束(loose)。
樣例 29Scaffold(
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[1],它告訴我們屏幕強制 Container 變得和屏幕一樣大。為何屏幕能夠做到這一點,原因就是給 Container 傳遞了嚴格約束。
一個寬鬆約束換句話來說就是設置了最大寬度/高度,但是讓允許其子 widget 獲得比它更小的任意大小。換句話來說,寬鬆約束的最小寬度/高度為 0。
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;如果你訪問 樣例 3[2],它將會告訴我們 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() 方法,由該方法執行列布局。
致謝本文的順利發布需感謝以下社區成員的支持:Xinlei Wang, Marcelo Glasberg, Simon Lightfoot, Luke, Alex, CaiJingLong, Yujie Ren, Kai Sun, Lynn Wang, Yisheng Xu,謝謝!
文中圖片因排版效果使用圓角顯示,實際效果無圓角,特此說明。如果您對本文有任何建議和討論,請點擊閱讀原文到 GitHub Issue 中反饋,因為外鏈限制,請到 flutter.cn 查看外部連結:flutter.cn/docs/development/ui/layout/constraints