理解Flutter Constraints

image.png

如果有人問你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:


image.png

那么整個布局和尺寸的確定流程如下:

  • 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:

  • 自身盡可能大的類型。例如CenterListView 使用的box。
  • 保持和children一樣大的類型。例如TransformOpacity使用的box。
  • 使用確定size的類型。例如ImageText 使用的box。

有一些Widget比如Container,會根據構造函數傳參的不同產生不同的約束處理方式。比如,如果使用默認構造函數,Container 會使用自身盡可能大的類型,但是如果傳了width、height 那么會盡量使用確定size類型。

舉例

接下來將通過29個例子來具體分析之前講的理論。不用擔心例子過多,例子之間的邏輯關聯最終會串聯他們之間的關系并推導出簡潔的結論。

例1

image.png
Container(color: red)

屏幕尺寸就是Container 的父Widget尺寸,所以Container 和屏幕的size一樣大。

例2

image.png
Container(width: 100, height: 100, color: red)

紅色的Container 想要自身大小為100*100,但是實際的大小被限制為屏幕大小。

例3

image.png
Center(child: Container(width: 100, height: 100, color: red))

屏幕限制Center 為屏幕大小,所以Center占滿屏幕。
Center 限制Container 為任何大小,但是不要超過屏幕。這時,Container 大小是100*100。

例4

image.png
Align(
  alignment: Alignment.bottomRight,
  child: Container(width: 100, height: 100, color: red),
)

這里和前一個例子用Center 不同的是使用了Align
AlignCenter 類似,Container 可以是任何大小 。但是布局在父容器的右下角。

例5

image.png
Center(
  child: Container(
    width: double.infinity,
    height: double.infinity,
    color: red,
  ),
)

屏幕限制Center 為屏幕大小,所以Center 占滿屏幕。
Center 限制Container 可以是不超過屏幕size的任何size。Container 想要無限大,但是受到不超過屏幕size的限制,所以只能是屏幕size。

Example6

image.png
Center(child: Container(color: red))

屏幕限制Center的大小為屏幕尺寸,所以Center 占滿屏幕。
Center 限制Container 的大小為任何不超過屏幕的大小。因為Container沒有限制大小也沒有任何child,所以這里會盡可能大,占滿屏幕。

例7

image.png
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

image.png
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

image.png
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

image.png
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 10, height: 10),
  ),
)

CenterConstrainedBox的大小限制不超過 屏幕大小。ConstrainedBoxconstraints中獲取附加限制并傳遞給它的child。
Container 的長寬都必須在70-150像素之間。自身想要的大小是10X10,最終結果是70X70。

例11

image.png
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 1000, height: 1000),
  ),
)

CenterConstrainedBox的大小限制不超過 屏幕大小。ConstrainedBoxconstraints中獲取附加限制并傳遞給它的child。
Container 的長寬都必須在70-150像素之間。自身想要的大小是1000X1000,最終結果是150X150。

例12

image.png
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 100, height: 100),
  ),
)

CenterConstrainedBox的大小限制不超過 屏幕大小。ConstrainedBoxconstraints中獲取附加限制并傳遞給它的child。
Container 的長寬都必須在70-150像素之間。自身想要的大小是100X100,在限制范圍內,最終結果是100X100。

例13

image.png
UnconstrainedBox(
  child: Container(color: red, width: 20, height: 50),
)

UnconstrainedBox 的大小是屏幕大小。但是UnconstrainedBox 的child可以是任何大小。

例14

image.png
UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)

UnconstrainedBox 的大小是屏幕大小。UnconstrainedBox 的child Container可以是任何大小。
但是這里Container寬度是4000像素,超過了UnconstrainedBox的大小,所以UnconstrainedBox顯示了“overflow warining”。

例15

image.png
OverflowBox(
  minWidth: 0,
  minHeight: 0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
  child: Container(color: red, width: 4000, height: 50),
)

OverflowBox 的大小為屏幕的大小,OverflowBox 的child可以是任何大小。
OverflowBoxUnconstrainedBox 很像,區別是OverflowBox 的child如果size超過了OverflowBox 的大小不會顯示overflow警告。

例16

image.png
UnconstrainedBox(
  child: Container(color: Colors.red, width: double.infinity, height: 100),
)

這里不會渲染任何內容,并且會在console中看到錯誤log輸出。
UnconstrainedBox 的child可以是任何大小,但是child的寬度是無限。Flutter不能渲染無限大小的Widget,所以會拋出BoxConstraints forces an infinite width 異常。

例17

image.png
UnconstrainedBox(
  child: LimitedBox(
    maxWidth: 100,
    child: Container(
      color: Colors.red,
      width: double.infinity,
      height: 100,
    ),
  ),
)

和例16相比,這里不會報錯,因為LimitedBoxUnconstrainedBox 限制上給出了一個有限的限制,寬度最多100。
如果 用 Center 取代UnconstrainedBoxLimitedBox不會再附加任何限制,因為LimitedBox 收到了一個有限制的約束,LimitedBox 本身的限制不會生效,所以Container的寬度允許超過100。
這里顯示了UnconstrainedBoxLimitedBox 區別。

例18

image.png
const FittedBox(child: Text('Some Example Text.'))

FittedBox 的大小是屏幕的大小。Text 的大小由自身顯示內容及性質決定。
FittedBox對于Text 的大小沒有限制,但是在Text 確定了大小后,FittedBox會縮放Text 直到占滿剩下空間。

例19

image.png
const Center(child: FittedBox(child: Text('Some Example Text.')))

如果將FittedBox放到 Center 里面呢? Center 限制FittedBox 的最大大小是屏幕大小。
FittedBox 根據Text 的size確定自身的size。這樣FittedBoxText 大小相同,無需縮放。

例20

image.png
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.',
    ),
  ),
)

如果FittedBoxCenter 里面,但是Text 的內容非常長呢?
FittedBox 嘗試根據Text 的大小來設置自身的大小,但是不能超過屏幕的大小。最終采用的是屏幕的size來resize Text 的大小。

例21

image.png
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.',
  ),
)

如果移除了FittedBoxText 的寬度限制就是屏幕的寬度,所以發生了換行來適配屏幕的寬度。

例22

image.png
FittedBox(
  child: Container(height: 20, width: double.infinity, color: Colors.red),
)

FittedBox 只能縮放有固定寬高的widget,否則不會渲染內容,并在console中輸出錯誤。

例23

image.png
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

image.png
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

image.png
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

image.png
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

image.png
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的寬度和自己一樣。但是FlexibleExpanded 在測量自身寬度的時候都會忽略child的寬度。

例28

image.png
Scaffold(
  body: Container(
    color: blue,
    child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
  ),
)

Scaffold 被強制使用屏幕的大小,所以Scaffold 會占滿屏幕。ScaffoldContainer 的限制是不超過屏幕的任何大小。

注意
當約束是不超過某個確定的大小時,就是所謂的loose constraints。

例29

image.png
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。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容