如果有人問你Flutter中某個widget設置了屬性width: 100
,但是顯示的并不是100像素。通常最直接的答案就是把這個widget放到 Center
中。但這樣真的對嗎?
不要這樣回答!
如果這樣回答,那么后續別人會不停地來問為什么FittedBox
表現不正常,Column
為什么顯示overflow了,或者IntrinsicWidth
是干什么用的?
正確的答案是,首先告訴他們Flutter的布局方式和HTML不同(如果對方碰巧是前端開發),然后告訴他們記住下面的規則:
約束向下傳遞。尺寸向上傳遞。父控件設置位置
理解上面這個規則是理解Flutter布局的關鍵,Flutter開發者應當盡早的理解上述規則。
詳細說明:
- widget接收來自父widget的約束。約束由4個double組成:最大、最小寬度和最大、最小高度。
- widget遍歷自己的子widget。將每個相應的約束傳遞給相應的子widget,然后詢問每個子widget本身想要的大小。
- 然后,widget通過x,y坐標將子widget放到布局中
- 最后,widget告訴他的父widget自身的大小(當然,還有開始的布局約束)
舉例,如果一個widget組合包含一個有padding的column里面有兩個child:
那么整個布局和尺寸的確定流程如下:
- Column:“Hey,父widget,我的約束是多少?”
- Column的父Widget: “你的寬度是0-300像素,高度是0-85像素”
- Column:“我自身有5個像素的padding,那么我的child最多可以有290像素寬,75像素高”
- Column:“First Child,你的約束是0-290像素寬,0-75像素高”
- First Child:“好的,我自己想要的size是290像素寬,20像素高”
- Column:“我還有一個child,那么他的約束是0-290像素寬,55像素高(75-20 =55)”
- Column:“Second Child, 你的約束是0-290像素寬,0-55像素高”
- Second Child:“OK,我自己想要的size是140像素寬,30像素高”
-
Column:“好的,那么我的First Child的位置是
x:5,y:5
,我的Second Child的位置是x:80,y:25
” - Column:"我的父Widget,我確定了自身的大小是300像素寬,60像素高"
限制
Flutter的布局引擎被設計成單次完成布局。雖然這種方法效率非常高,但是也會存在一些限制:
- Widget只能根據父Widget給出的約束來確定自身的大小。這會導致widget無法完全按自身的邏輯設置自身的大小。
- Widget無法知道也無法決定自身在屏幕中的位置,因為只有父widget才能確定子widget的位置。
- 由于父Widget也依賴父父Widget確定自身的大小和位置,只有在遍歷整個布局樹后才能精確獲取Widget的大小和位置。
- 如果子Widget的size和父Widget的size不同,父Widget又沒有足夠的信息來對齊子Widget,那么子Widget的size會被忽略。定義aligment的時候需要當心。
Flutter中,Widget由底層的RenderBox
渲染。Flutter中的許多box,特別是只有一個child的box,都會將約束傳遞給他們的child。
通常根據處理約束的方式不同分為三種box:
- 自身盡可能大的類型。例如
Center
和ListView
使用的box。 - 保持和children一樣大的類型。例如
Transform
和Opacity
使用的box。 - 使用確定size的類型。例如
Image
和Text
使用的box。
有一些Widget比如Container
,會根據構造函數傳參的不同產生不同的約束處理方式。比如,如果使用默認構造函數,Container
會使用自身盡可能大的類型,但是如果傳了width、height
那么會盡量使用確定size類型。
舉例
接下來將通過29個例子來具體分析之前講的理論。不用擔心例子過多,例子之間的邏輯關聯最終會串聯他們之間的關系并推導出簡潔的結論。
例1
Container(color: red)
屏幕尺寸就是Container
的父Widget尺寸,所以Container
和屏幕的size一樣大。
例2
Container(width: 100, height: 100, color: red)
紅色的Container
想要自身大小為100*100,但是實際的大小被限制為屏幕大小。
例3
Center(child: Container(width: 100, height: 100, color: red))
屏幕限制Center
為屏幕大小,所以Center
占滿屏幕。
Center
限制Container
為任何大小,但是不要超過屏幕。這時,Container
大小是100*100。
例4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
這里和前一個例子用Center
不同的是使用了Align
。
Align
和Center
類似,Container
可以是任何大小 。但是布局在父容器的右下角。
例5
Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: red,
),
)
屏幕限制Center
為屏幕大小,所以Center
占滿屏幕。
Center
限制Container
可以是不超過屏幕size的任何size。Container
想要無限大,但是受到不超過屏幕size的限制,所以只能是屏幕size。
Example6
Center(child: Container(color: red))
屏幕限制Center
的大小為屏幕尺寸,所以Center
占滿屏幕。
Center
限制Container
的大小為任何不超過屏幕的大小。因為Container
沒有限制大小也沒有任何child,所以這里會盡可能大,占滿屏幕。
例7
Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
屏幕限制Center
的大小為屏幕尺寸,所以Center
占滿屏幕。
Center
限制Container
的大小為任何不超過屏幕的大小。Container
沒有指定的size但是有一個child,所以會把自身的大小設置為child的大小。
紅色的Container
限制child可以是不超過屏幕的任何大小。
綠色的Container
child大小是30X30。所以紅色的Container
大小是30X30。這里紅色的Container
不可見是因為被綠色的Container
正好蓋住了。
例8
Center(
child: Container(
padding: const EdgeInsets.all(20),
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
紅色的Container
的size就是child的size加上padding。即size是70X70。
例9
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
)
根據代碼字面上的意思是Container
的size是在70X70到150X150之間,但是這是錯誤的。ConstrainedBox
的constraints參數是在父Widget傳來的約束上附加的額外約束。
這里ConstrainedBox
大小是屏幕的大小,所以它傳遞給child Container
的約束也是屏幕的大小,所以constraints
參數被忽略了。
例10
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
),
)
Center
給ConstrainedBox
的大小限制不超過 屏幕大小。ConstrainedBox
從constraints
中獲取附加限制并傳遞給它的child。
Container
的長寬都必須在70-150像素之間。自身想要的大小是10X10,最終結果是70X70。
例11
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 1000, height: 1000),
),
)
Center
給ConstrainedBox
的大小限制不超過 屏幕大小。ConstrainedBox
從constraints
中獲取附加限制并傳遞給它的child。
Container
的長寬都必須在70-150像素之間。自身想要的大小是1000X1000,最終結果是150X150。
例12
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 100, height: 100),
),
)
Center
給ConstrainedBox
的大小限制不超過 屏幕大小。ConstrainedBox
從constraints
中獲取附加限制并傳遞給它的child。
Container
的長寬都必須在70-150像素之間。自身想要的大小是100X100,在限制范圍內,最終結果是100X100。
例13
UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
)
UnconstrainedBox
的大小是屏幕大小。但是UnconstrainedBox
的child可以是任何大小。
例14
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
UnconstrainedBox
的大小是屏幕大小。UnconstrainedBox
的child Container
可以是任何大小。
但是這里Container
寬度是4000像素,超過了UnconstrainedBox
的大小,所以UnconstrainedBox
顯示了“overflow warining”。
例15
OverflowBox(
minWidth: 0,
minHeight: 0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: red, width: 4000, height: 50),
)
OverflowBox
的大小為屏幕的大小,OverflowBox
的child可以是任何大小。
OverflowBox
和UnconstrainedBox
很像,區別是OverflowBox
的child如果size超過了OverflowBox
的大小不會顯示overflow警告。
例16
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
這里不會渲染任何內容,并且會在console中看到錯誤log輸出。
UnconstrainedBox
的child可以是任何大小,但是child的寬度是無限。Flutter不能渲染無限大小的Widget,所以會拋出BoxConstraints forces an infinite width
異常。
例17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
和例16相比,這里不會報錯,因為LimitedBox
在UnconstrainedBox
限制上給出了一個有限的限制,寬度最多100。
如果 用 Center
取代UnconstrainedBox
,LimitedBox
不會再附加任何限制,因為LimitedBox
收到了一個有限制的約束,LimitedBox
本身的限制不會生效,所以Container
的寬度允許超過100。
這里顯示了UnconstrainedBox
和 LimitedBox
區別。
例18
const FittedBox(child: Text('Some Example Text.'))
FittedBox
的大小是屏幕的大小。Text
的大小由自身顯示內容及性質決定。
FittedBox
對于Text
的大小沒有限制,但是在Text
確定了大小后,FittedBox
會縮放Text
直到占滿剩下空間。
例19
const Center(child: FittedBox(child: Text('Some Example Text.')))
如果將FittedBox
放到 Center
里面呢? Center
限制FittedBox
的最大大小是屏幕大小。
FittedBox
根據Text
的size確定自身的size。這樣FittedBox
和 Text
大小相同,無需縮放。
例20
const 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
的大小來設置自身的大小,但是不能超過屏幕的大小。最終采用的是屏幕的size來resize Text
的大小。
例21
const 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, width: double.infinity, color: Colors.red),
)
FittedBox
只能縮放有固定寬高的widget,否則不會渲染內容,并在console中輸出錯誤。
例23
Row(
children: [
Container(color: red, child: const Text('Hello!', style: big)),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
Row
的大小就是屏幕的大小。和UnconstrainedBox
一樣,Row
不會限制child的大小,所以child可以是自身想要的任何大小。
例24
Row(
children: [
Container(
color: red,
child: const Text(
'This is a very long text that '
'won\'t fit the line.',
style: big,
),
),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
因為Row
不會給child任何限制,所以child很容易就因為太長超出Row
的寬度。所以這里和 UnconstrainedBox
一樣,顯示了"overflow warning"。
例25
Row(
children: [
Expanded(
child: Center(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
當Row
的child被Expanded
包裹的時候,Row
就不會讓child自身決定自身的大小了。這時,會根據其他child的大小來確定Expanded
的寬度,然后Expanded
會強制被包裹的child使用Expanded
的大小。
換句話說,如果使用了Expanded
,內部包裹的child的寬度會被忽略。
例26
Row(
children: [
Expanded(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
Expanded(
child: Container(
color: green,
child: const Text('Goodbye!', style: big),
),
),
],
)
如果Row
所有的child都用Expanded
包裹,那么每個Expanded
的size和它的flex參數大小正比,并且就是每個Expanded
包裹的child大小。換言之,Expanded
忽略了child自身的大小。
例27
Row(
children: [
Flexible(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
Flexible(
child: Container(
color: green,
child: const Text('Goodbye!', style: big),
),
),
],
)
這里唯一的區別就是使用Flexible
代替了Expanded
,這樣child的寬度就可以小于等于Flexible
的寬度,而不是像Expanded
一樣強制child的寬度和自己一樣。但是Flexible
和Expanded
在測量自身寬度的時候都會忽略child的寬度。
例28
Scaffold(
body: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
)
Scaffold
被強制使用屏幕的大小,所以Scaffold
會占滿屏幕。Scaffold
給Container
的限制是不超過屏幕的任何大小。
注意
當約束是不超過某個確定的大小時,就是所謂的loose constraints。
例29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
),
)
如果想要Scaffold
的child和Scaffold
大小一樣,那么可以使用SizedBox.expand
包裹這個child。
Tight vs loose約束
Tight 約束
所謂的tight約束就是有精確的大小。換句話說就是tight約束的最大寬/高等于最小寬/高。
最常見的例子就是包含在RenderView
類中的App
widget:應用的build
方法返回的box,使用的就是tight約束,大小就是屏幕的大小。
另一個例子是,如果你在應用的render tree的root中嵌套box,每個box都被強制使用tight 約束,這樣就能完全填充。
如果看一下box.dart
的源碼,搜索BoxConstraints
構造函數:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
和前面講的例2一樣,屏幕強制Container
使用屏幕的大小,所以Container
的大小被忽略了。
Loose 約束
loose約束就是最大寬/高不等于0,最小寬/高等于0。
例如Center
這樣的box,就會loose從父控件收到的約束。例3中,Center
的child Container
就可以小于Center
的大小(但是也不能超過屏幕傳遞個Center的大小)。
Unbounded 約束
在某些情況下,box的約束是unbounded 或者infinite 的。也就是說寬或者高被設為 double.infinity
。
如果一個自身為盡可能大的box的約束是Unbounded約束,那么在debug 模式下,會拋出異常。
最常見的例子就是在flex box(例如Row或者Column)或者可滾動區域(例如LIstView或者其他ScrollView子類)里面有一個Unbounded約束的render box。
Flex
具體到Flex box(例如Row或者Column)Unbounded約束的影響還取決于是否在發生在主方向上(Row的寬,Column的高)。
Flex box在bounded約束的情況下,在主方向上盡可能大。
Flex box在Unbounded約束的情況下,會盡可能滿足child在這個方向上的大小。每個child的flex 值都需要被設置為0,也就是說無法在Unbounded約束的flex box或者scrollable里使用Expanded,否則會拋出異常。
Flex box的交叉方向(Row的高,Column的寬)必須是bounded,否則無法布局內部的child。