原文:Flutter: The Advanced Layout Rule Even Beginners Must Knowhtml
做者:Marcelo Glasberggit
譯者:Vadaskigithub
校對:Luke Cheng、Alexapi
這篇文章最初來自於 Marcelo Glasberg 在 Medium 發表的 Flutter: The Advanced Layout Rule Even Beginners Must Know。後被 Flutter Team 發現並收錄到 flutter.dev。app
在認真閱讀完這篇文章後,我認爲它對 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: "你的寬度必須在 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 倉庫中 獲取其代碼。
如下各節將介紹這些示例。
Container(color: Colors.red)
複製代碼
整個屏幕做爲 Container
的父級,而且強制 Container
變成和屏幕同樣的大小。
因此這個 Container
充滿了整個屏幕,並繪製成紅色。
Container(width: 100, height: 100, color: Colors.red)
複製代碼
紅色的 Container
想要變成 100 x 100 的大小, 可是它沒法變成,由於屏幕強制它變成和屏幕同樣的大小。
因此 Container
充滿了整個屏幕。
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
複製代碼
屏幕強制 Center
變得和屏幕同樣大,因此 Center
充滿了屏幕。
而後 Center
告訴 Container
能夠變成任意大小,可是不能超出屏幕。 如今,Container
能夠真正變成 100 × 100 大小了。
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
複製代碼
與上一個樣例不一樣的是,咱們使用了 Align
而不是 Center
。
Align
一樣也告訴 Container
,你能夠變成任意大小。 可是,若是還留有空白空間的話,它不會居中 Container
。 相反,它將會在容許的空間內,把 Container
放在右下角(bottomRight)。
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
複製代碼
屏幕強制 Center
變得和屏幕同樣大,因此 Center
充滿屏幕。
而後 Center
告訴 Container
能夠變成任意大小,可是不能超出屏幕。 如今,Container
想要無限的大小,可是因爲它不能比屏幕更大, 因此就僅充滿屏幕。
Center(child: Container(color: Colors.red))
複製代碼
屏幕強制 Center
變得和屏幕同樣大,因此 Center
充滿屏幕。
而後 Center
告訴 Container
能夠變成任意大小,可是不能超出屏幕。 因爲 Container
沒有子級並且沒有固定大小,因此它決定能有多大就有多大, 因此它充滿了整個屏幕。
可是,爲何 Container
作出了這個決定? 很是簡單,由於這個決定是由 Container
widget 的建立者決定的。 可能會因創造者而異,並且你還得閱讀 Container
文檔 來理解不一樣場景下它的行爲。
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
, 因此你看不見它了。
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
則仍是和以前同樣。
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
參數帶來的影響。
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)。
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)。
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 個像素。
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
複製代碼
屏幕強制 UnconstrainedBox
變得和屏幕同樣大,而 UnconstrainedBox
容許其子級的 Container
能夠變爲任意大小。
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
複製代碼
屏幕強制 UnconstrainedBox
變得和屏幕同樣大, 而 UnconstrainedBox
容許其子級的 Container
能夠變爲任意大小。
不幸的是,在這種狀況下,容器的寬度爲 4000 像素, 這實在是太大,以致於沒法容納在 UnconstrainedBox
中, 所以 UnconstrainedBox
將顯示溢出警告(overflow warning)。
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
會所有顯示,而不會發出警告。
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
複製代碼
這將不會渲染任何東西,並且你能在控制檯看到錯誤信息。
UnconstrainedBox
讓它的子級決定成爲任何大小, 可是其子級是一個具備無限大小的 Container
。
Flutter 沒法渲染無限大的東西,因此它拋出如下錯誤: BoxConstraints forces an infinite width.
(盒子約束強制使用了無限的寬度)
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
複製代碼
此次你就不會遇到報錯了。 UnconstrainedBox
給 LimitedBox
一個無限的大小; 但它向其子級傳遞了最大爲 100 的約束。
若是你將 UnconstrainedBox
替換爲 Center
, 則LimitedBox
將再也不應用其限制(由於其限制僅在得到無限約束時才適用), 而且容器的寬度容許超過 100。
上面的樣例解釋了 LimitedBox
和 ConstrainedBox
之間的區別。
FittedBox(
child: Text('Some Example Text.'),
)
複製代碼
屏幕強制 FittedBox
變得和屏幕同樣大, 而 Text
則是有一個天然寬度(也被稱做 intrinsic 寬度), 它取決於文本數量,字體大小等因素。
FittedBox
讓 Text
能夠變爲任意大小。 可是在 Text
告訴 FittedBox
其大小後, FittedBox
縮放文本直到填滿全部可用寬度。
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
複製代碼
但若是你將 FittedBox
放進 Center
widget 中會發生什麼? Center
將會讓 FittedBox
可以變爲任意大小, 取決於屏幕大小。
FittedBox
而後會根據 Text
調整本身的大小, 而後讓 Text
能夠變爲所需的任意大小, 因爲兩者具備同一大小,所以不會發生縮放。
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
的大小以使其也適合屏幕。
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
則會從屏幕上獲取其最大寬度, 並在合適的地方換行。
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
複製代碼
FittedBox
只能在有限制的寬高中 對子 widget 進行縮放(寬度和高度不會變得無限大)。 不然,它將沒法渲染任何內容,而且你會在控制檯中看到錯誤。
Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
複製代碼
屏幕強制 Row
變得和屏幕同樣大,因此 Row
充滿屏幕。
和 UnconstrainedBox
同樣, Row
也不會對其子代施加任何約束, 而是讓它們成爲所需的任意大小。 Row
而後將它們並排放置, 任何多餘的空間都將保持空白。
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
同樣顯示溢出警告。
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
, 子級自身的寬度就變得可有可無,直接會被忽略掉。
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
想要的寬度。
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
從而忽略子級的寬度。
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
複製代碼
屏幕強制 Scaffold
變得和屏幕同樣大, 因此 Scaffold
充滿屏幕。 而後 Scaffold
告訴 Container
能夠變爲任意大小, 但不能超出屏幕。
當一個 widget 告訴其子級能夠比自身更小的話, 咱們一般稱這個 widget 對其子級使用 寬鬆約束(loose)。
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
複製代碼
若是你想要 Scaffold
的子級變得和 Scaffold
自己同樣大的話, 你能夠將這個子級外包裹一個 SizedBox.expand
。
當一個 widget 告訴它的子級必須變成某個大小的時候, 咱們一般稱這個 widget 對其子級使用 嚴格約束(tight)。
之後你常常會聽到一些約束爲嚴格約束或寬鬆約束, 你花點時間來弄明白它們是值得的。
嚴格約束給你了一種得到確切大小的選擇。 換句話來講就是,它的最大/最小寬度是一致的,高度也同樣。
若是你到 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 的行爲。
佈局源代碼一般很複雜,所以閱讀文檔是更好的選擇。 可是當你在研究佈局源代碼時,可使用 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!