對於Flutter學習者來說,掌握Flutter的佈局行爲,直接決定了開發者在佈局的時候是否能做到高效、快速的開發,但是初學者面對茫茫多的Widget以及各種無法預料的佈局行爲,總是很難將心中所想,轉化爲Flutter的代碼。
本文翻譯整理自https://flutter.dev/docs/development/ui/layout/constraints
順便插句話,我的開源項目Flutter_dojo,剛發佈了2.0,歡迎大家體驗。
Flutter_dojo
歡迎大家體驗。
當學習Flutter的人問你,爲什麼寬度爲100的某些小部件在顯示的時候,寬度不爲100像素時,你的默認答案是告訴他們將小部件放在Center內,對嗎?
不要這樣做。如果這樣做,他們會一次又一次地回來,詢問爲什麼某些FittedBox不起作用,爲什麼Column溢出了,或者IntrinsicWidth應該做什麼。
相反,請先告訴他們Flutter佈局與HTML佈局(可能是他們非常熟悉的)有很大不同,然後讓他們記住以下規則:
Constraints go down. Sizes go up. Parent sets position。
如果不瞭解此規則,就無法真正理解Flutter的佈局,因此Flutter開發人員應儘早學習。
更詳細地:
Widget從其父級獲得自己的約束。約束只是一組4個雙精度數:
- 最小和最大寬度
- 最小和最大高度
然後Widget遍歷它的所有子Widget。Widget一個接一個地告訴其孩子約束(每個孩子可能有所不同),然後詢問每個孩子想要的大小,然後,Widget將其孩子定位(水平地在x軸上佈局,垂直地在y軸上佈局),最後,該小部件將其自身的大小告訴父級(當然,在原始約束內)。
例如,如果一個組合Widget包含帶有一些Padding和Column,並且希望如圖所示佈置其兩個Widget:
談判是這樣的:
- Widget:嗨,Parent,我的約束是什麼?
- Parent Widget:你的寬度必須在80到300像素之間,而高度必須在30到85像素之間。
- Widget:嗯,因爲我要有5像素的Padding,所以我的子Widget最多可以有290像素的寬度和75像素的高度。
- Widget:嗨,第一個子Widget,你的寬度必須在0到290像素之間,並且必須在0到75高之間。
- First child:好,那我希望寬290像素,高20像素。
- Widget:嗯,由於我想將第二個子Widget放到第一個子Widget下面,所以第二個子Widget只剩下55像素的高度。
- Widget:嗨,第二個子Widget,你的高度必須在0到290之間,並且必須在0到55高之間。
- Second child:好吧,我希望寬140像素,高30像素。
- Widget:很好。我的第一個孩子的位置x:5和y:5,第二個孩子的位置x:80和y:25。
- Widget:親愛的父母,我決定將尺寸設爲300像素寬,60像素高。
Limitations
由於上述佈局規則,Flutter的佈局引擎具有一些重要限制:
- Widget只能在其父級賦予的限制內決定其自身大小。這意味着Widget通常不能具有所需的任何大小。佈局是自上而下,當前widget會有基本的一些約束(來自它的父元素),主要是關於寬高的最小值和最大值
- Widget無法知道也不決定其在屏幕上的位置,因爲Widget的父級決定小部件的位置。它會依次詢問子元素關於佈局的基本限制要求,讓子元素上報期望的佈局結果,然後根據現狀和自己佈局算法的特點,告訴子元素應該放到那兒,佔多大空間
由於父級的大小和位置又取決於其父級,因此在不考慮整個樹的情況下就無法精確定義任何小部件的大小和位置。
每個widget不一定會得到它期望的佈局大小,這方面顯著的例子是ConstrainedBox,很容易讓人困惑。
每個widget不能決定在屏幕中的位置,由父元素決定
因爲這種佈局邏輯需要層層考慮上層元素,所以一個元素的最終佈局需要考慮整個UI裏widget樹。
如果爲了精確局部佈局,Container和ConstrainedBox會是一個可行的修飾佈局。
Examples
下面的29個示例,將演示Flutter的佈局思想。
https://github.com/marcglasberg/flutter_layout_article
Example 1
Container(color: Colors.red)
屏幕是Container的父級,它強制容器與屏幕的尺寸完全相同。
因此,容器將屏幕填滿並塗成紅色。
Example 2
Container(width: 100, height: 100, color: Colors.red)
想要紅色的容器爲100×100,但不是,因爲屏幕會強制使其尺寸與屏幕完全相同。
因此,容器充滿了屏幕。
Example 3
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
屏幕會強制Center與屏幕完全相同,因此Center會填滿整個屏幕。
Center告訴Container它可以是所需的任何大小,但不能大於屏幕大小。
所以現在容器確實可以是100×100。
Example 4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
這與上一個示例不同,因爲它使用Align而不是Center。
Align同樣告訴Container它可以是任何所需的大小,同時會在剩餘的可用空間中bottom-right對齊。
Example 5
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
屏幕會強制Center與屏幕完全相同,因此Center會填滿整個屏幕。
Center告訴Container它可以是所需的任何大小,但不能大於屏幕大小。
容器希望具有無限大小,但由於不能大於屏幕,因此只能填充屏幕。
Example 6
Center(child: Container(color: Colors.red))
屏幕會強制Center與屏幕完全相同,因此Center會填滿整個屏幕。
Center告訴Container它可以是所需的任何大小,但不能大於屏幕大小。
由於該Container沒有Child且沒有固定的大小,因此它決定要儘可能大,因此將其填滿整個屏幕。
但是Container爲什麼要這樣決定呢?僅僅是因爲這是創建Container的人的設計決定。
其它的Widget的創建方式可能有所不同,具體取決於情況。
Example 7
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
屏幕會強制Center與屏幕完全相同,因此Center會填滿整個屏幕。
Center告訴紅色Container它可以是所需的任何大小,但不大於屏幕。
由於紅色的Container沒有大小,但是有一個Child,因此它決定要與孩子的大小相同。
紅色的Container告訴其子項可以是它想要的任何大小,但不能大於屏幕大小。
這個Child是一個綠色的Container,它希望是30×30。考慮到紅色Container的大小與其孩子的大小相同,它也是30×30,所以紅色是不可見的,因爲綠色的Container會完全覆蓋紅色Container。
Example 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×30加上padding。 由於有padding,因此可以看到紅色,綠色Container與上一個示例中的大小相同。
Example 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也假定屏幕大小,從而忽略了其約束參數。
Example 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將來自其約束參數的附加約束施加到其子對象上。
Container必須介於70到150像素之間。 它希望有10個像素,所以最終有70個像素(最小)。
Example 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將來自其約束參數的附加約束施加到其子對象上。
Container必須介於70到150像素之間。 它希望有1000個像素,所以最終有150個(最大)。
Example 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將來自其約束參數的附加約束施加到其子對象上。
Container必須介於70到150像素之間。它希望有100像素,這就是它的大小,因爲它介於70到150之間。
Example 13
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
屏幕強制UnconstrainedBox與屏幕大小完全相同。 但是,UnconstrainedBox允許其子Container設置任意大小。
Example 14
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
屏幕強制UnconstrainedBox與屏幕大小完全相同,UnconstrainedBox將其子Container設爲任意大小。
不幸的是,在這種情況下,容器的寬度爲4000像素,太大而無法容納在UnconstrainedBox中,因此UnconstrainedBox顯示溢出警告。
Example 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類似,但不同的是,如果Child不適合該空間,它將不會顯示任何警告。
在這種情況下,容器的寬度爲4000像素,並且太大而無法容納在OverflowBox中,但是OverflowBox會盡可能地顯示儘可能多的內容,而不會發出警告。
Example 16
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
你會在控制檯中看到錯誤。
UnconstrainedBox可以讓它的子Widget具有所需的任何大小,但是其子Widget是一個具有無限大小的Container。
Flutter無法呈現無限大小,因此會出現以下錯誤消息:BoxConstraints forces an infinite width.
Example 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之間的區別。
Example 18
FittedBox(
child: Text('Some Example Text.'),
)
屏幕將強制FittedBox與屏幕完全相同。
文本將根據寬度調整自有的寬度屬性,字體屬性等。
FittedBox允許文本的尺寸爲任意大小,但在將文本告知FittedBox大小後,FittedBox縮放文本直到填滿所有可用寬度。
Example 19
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
但是,如果將FittedBox放在Center內會怎樣?Center會將FittedBox設置爲所需的任何大小,直至屏幕大小。
然後,將FittedBox調整爲Text大小,並讓Text爲所需的任何大小。
由於FittedBox和Text具有相同的大小,因此不會發生縮放。
Example 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中,但文本太大而無法容納屏幕,會發生什麼?
FittedBox會嘗試根據文本大小調整大小,但不能大於屏幕大小。然後假定屏幕大小,並調整文本的大小以使其也適合屏幕。
Example 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從屏幕上獲取其最大寬度,並在合適 的地方換行。
Example 22
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
FittedBox只能在有限制的寬高中進行Child的縮放(寬度和高度非無限大)。 否則,它將無法呈現任何內容,並且你會在控制檯中看到錯誤。
Example 23
Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
屏幕強制行與屏幕大小完全相同。
就像UnconstrainedBox一樣,Row不會對其子代施加任何約束,而是讓它們成爲所需的任意大小。Row然後將它們並排放置,任何多餘的空間都將保持空白。
Example 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不會對其子級施加任何約束,因此子Widget很有可能太大而無法容納Row的可用寬度。
在這種情況下,就像UnconstrainedBox一樣,Row會顯示溢出警告。
Example 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的子Child被包裹在Expanded中時,Row將不再讓該Child定義自己的寬度。
取而代之的是,Row會根據所有Expanded的Child來計算其該有的寬度。
換句話說,一旦您使用Expanded,原始Widget的寬度就變得無關緊要,並且會被忽略。
Example 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的子Widget都包裝在Expeded中,則每個Expeded的大小均與其flex參數成比例,子Child會設置爲計算的Expanded寬度。
換句話說,Expanded忽略了其子Widget寬度。
Example 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使其子元素的寬度等於或小於其自身的寬度,而Expanded強制其子元素具有與Expeded完全相同的寬度。
但是,在調整尺寸時,Expanded和Flexible的都忽略了孩子的寬度。
注意:這意味着,Row要麼使用子Child的寬度,要麼使用Expanded和Flexible從而忽略Child的寬度。
Example 28
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
屏幕會強制設置Scaffold與屏幕大小完全相同,因此Scaffold會填滿屏幕。
Scaffold告訴容器它可以是所需的任何大小,但不能大於屏幕大小。
注意:當Widget告訴其子Widget它可以小於特定大小時,我們說該Widget爲其Child提供了loose約束。
Example 29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
如果你希望Scaffold的子Widget與自己的Scaffold大小完全相同,則可以使用SizedBox.expand包裝其Child。
注意:當小部件告訴其子級必須具有一定大小時,我們說該小部件爲其子級提供了tight約束。
Tight vs loose constraints
前面經常提到一些約束是tight或loose,所以你值得知道這是什麼意思。
tight constraint提供了一種可能性,即確切的大小。換句話說,tight constraint的最大寬度等於其最小寬度。 並且其最大高度等於其最小高度。
如果轉到Flutter的box.dart文件並搜索BoxConstraints構造函數,則會發現以下內容:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
如果你重新查看上面的示例2,它將告訴我們屏幕強制紅色Container與屏幕完全相同。
當然,屏幕是通過將tight constraint傳遞給Container來實現的。
另一方面,寬鬆的約束設置了最大寬度和高度,但使小部件儘可能小。 換句話說,寬鬆約束的最小寬度和高度都等於零:
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
如果您重新查看示例3,它將告訴我們Center使紅色Container變得更小,但不大於屏幕。
Center通過向Container傳遞loose constraint來做到這一點。
最終,Center的主要目的是將其從父級(屏幕)獲得的tight constraint轉換爲對其子級(容器)的loose constraint。
Learning the layout rules for specific widgets
知道一般的佈局規則是必要的,但這還不夠。
每個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的渲染對象。現在導航到RenderFlex的源代碼,將您帶到flex.dart文件。
向下滾動,直到找到一個名爲performLayout()的方法。 這是執行列布局的方法。
對Flutter感興趣的朋友可以加入我的Flutter修仙羣。