關於 Flutter Layout 你應該知道的

收藏待读

關於 Flutter Layout 你應該知道的

這篇文章首發於 Medium ,略顯生硬的英文看來並不太妨礙理解。

與 Flutter 的布局系統搏鬥一段時間之後,感覺終於找到了點門道,於是花了點時間整理了下。

核心概念

Unbounded Constraints

either the maximum width or the maximum height is set to double.INFINITY

關於 Flutter Layout 你應該知道的

ScrollView 和它的子類比如 ListViewGridView 是常見的 Unbounded Constraints. 也就是在某一個方向沒有限制大小。其他的 widget 只要能夠設置 widthheightdouble.INFINITY 的也算。有時也會用 as big as possible 來描述這些 widgets。

Flex

when in bounded constraints, try to be as big as possible in that direction.
when in unbounded constraints, try to fit their children in that direction.

當在有限的空間內,會撐滿整個空間;如果在一個 unbounded constraints 容器里,就匹配子 widget 的大小。

關於 Flutter Layout 你應該知道的

最常見的是 RowColumn ,如果不嫌麻煩的話,也可以使用 Flex widget。裏面可以放 Flexible widget,也可以不是。如果有 Flexible widgets 會把剩餘空間計算出來分配給這些 widgets。

Flexible

Flex 搭配使用, Flexible 可以用來聲明使用百分之多少的空間。比如 flex = 1 也就是 1/all ,如果有兩個 widgets,另一個也是 1,那麼 all = 2 ,每個 widget 分配到 50% 的空間。

關於 Flutter Layout 你應該知道的

Expanded 是最常見的 Flexible widget,它會填滿主方向上可用的空間(比如 Row 的水平空間或 Column 的垂直空間)。

主要 Widgets

Container

Containers with no children try to be as big as possible unless the incoming constraints are unbounded, in which case they try to be as small as possible.
Containers with children size themselves to their children.
The width, height, and constraints arguments to the constructor override this.

這是 Container 的三個主要表現:當沒有子 widgets 且沒有指定 constraints 時,儘可能地充滿可用空間,如果有 constraints 就以 constraints 為準(除非跟 parent constraints 衝突);如果有子 widgets 則以 children 的 size 為準;可以設置 width , heightconstraints 來約束 size。

return MaterialApp(
  home: Scaffold(
    body: Container(
      color: Colors.yellow,
    ),
  ),
);

這是一個沒有孩子的 container,因此它會表現地盡量大,就像這樣:

關於 Flutter Layout 你應該知道的

如果設置了 widthheight ,則會根據設置的值來表現:

return MaterialApp(
  home: Scaffold(
    body: Container(
      color: Colors.yellow,
      width: 100,
      height: 100,
    ),
  ),
);

關於 Flutter Layout 你應該知道的

如果有 child,則會以 child 的 size 為準:

return MaterialApp(
  home: Scaffold(
    body: Container(
      color: Colors.yellow,
      child: Text('hello'),
    ),
  ),
);

關於 Flutter Layout 你應該知道的

除此之外,還可以設置 padding, margin, child 的對齊方式,等等。

Stack

Stack 有點像 css 的絕對布局,可以在上面蓋一些 widgets,比如 profile 頁的背景圖上放一些個人信息。

Each child of a Stack widget is either positioned or non-positioned.
Positioned children are those wrapped in a Positioned widget that has at least one non-null property.
The stack sizes itself to contain all the non-positioned children, which are positioned according to alignment.

Stack 的 children 如果沒有用 Positioned 修飾的話,就會用 Stack 的 fitalighment 來幫它們找到合適的位置。

Stack(
  fit: StackFit.loose,
  alignment: Alignment.center,
  children: [
    Text('world'),
    Positioned(
     bottom: 10,
     child: Text('hello'),
   )
 ],
),

關於 Flutter Layout 你應該知道的

StackFit.loose 的意思是,如果 child size 不比 Stack 的大,就用 child 的 size。而如果設置為 StackFit.expand 則會讓所有非 Positioned 的 widgets 使用 Stack 的 size。

關於 Flutter Layout 你應該知道的

Text('world') 現在就跟 Stack 一樣大了,所以看起來像是 alignment.center 沒有生效。

Row and Column

它們都是 Flex widgets,Row 可以將 children 橫着放,column 可以將 children 豎著放。

crossAxisAlignment 表示要如何對齊另一側,比如橫着一排的 widgets,垂直方向上它們應該頂部對齊還是居中對齊呢。

mainAxisSize 默認是 MainAxisSize.max ,如果想讓它變成 Row 或 Column 的真實高度,可以將它設置為 MainAxisSize.min

SizedBox

使用它可以得到一個確定尺寸的 widget。

SafeArea

使用 SafeArea 可以讓 child widget 在頂部和底部騰出足夠的空間方便處理 iPhoneX 這類的手機。

原則

不要在 Flex widget 里放置 unbounded constraints

Column 是 Flex widget,所以在裏面放 ListView 的話,系統不會答應的。

return MaterialApp(
  home: Column(
    children: [
      ListView.builder(
        itemBuilder: (context, index){
          return Text('hello');
        },
        itemCount: 3,
      )
    ],
  ),
);

系統會給出這樣的 error

flutter: The following assertion was thrown during performResize():
flutter: Vertical viewport was given unbounded height.
...

因為 Column 作為 Flex 它不知道應該如何安放一個 as big as possible 的 widget。解決方法也很簡單,只要設置 ListView 的 shrinkWrap=true 即可。這就是告訴 ListView 把自己儘可能地縮小。

可以在 ColumnRow 里使用 Expanded ,因為它是 Flexible ,就應該待在 Flex 裏面。

不要在 unbounded widgets 里設置 flex 為不等於 0 的數值

因為空間無限,如果兩個 Flexible 分別為 1 和 2,那麼 Flex 根本不知道該如何將空間劃分成 1:2。如果真這麼做的話,會收到這樣的 error:

...
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
...

小測驗

下面這段代碼會讓 Hello World 被包裹在中間的小方塊里嗎?

return MaterialApp(
  home: Container(
    alignment: Alignment.center,
    constraints: BoxConstraints.tight(Size(100, 100)),
    decoration: BoxDecoration(color: Colors.yellow),
    child: Text('Hello World'),
  ),
);

關於 Flutter Layout 你應該知道的

Text('world') 現在就跟 Stack 一樣大了,所以看起來像是 alignment.center 沒有生效。

Row and Column

它們都是 Flex widgets,Row 可以將 children 橫着放,column 可以將 children 豎著放。

crossAxisAlignment 表示要如何對齊另一側,比如橫着一排的 widgets,垂直方向上它們應該頂部對齊還是居中對齊呢。

mainAxisSize 默認是 MainAxisSize.max ,如果想讓它變成 Row 或 Column 的真實高度,可以將它設置為 MainAxisSize.min

SizedBox

使用它可以得到一個確定尺寸的 widget。

SafeArea

使用 SafeArea 可以讓 child widget 在頂部和底部騰出足夠的空間方便處理 iPhoneX 這類的手機。

原則

不要在 Flex widget 里放置 unbounded constraints

Column 是 Flex widget,所以在裏面放 ListView 的話,系統不會答應的。

return MaterialApp(
  home: Column(
    children: [
      ListView.builder(
        itemBuilder: (context, index){
          return Text('hello');
        },
        itemCount: 3,
      )
    ],
  ),
);

系統會給出這樣的 error

flutter: The following assertion was thrown during performResize():
flutter: Vertical viewport was given unbounded height.
...

因為 Column 作為 Flex 它不知道應該如何安放一個 as big as possible 的 widget。解決方法也很簡單,只要設置 ListView 的 shrinkWrap=true 即可。這就是告訴 ListView 把自己儘可能地縮小。

可以在 ColumnRow 里使用 Expanded ,因為它是 Flexible ,就應該待在 Flex 裏面。

不要在 unbounded widgets 里設置 flex 為不等於 0 的數值

因為空間無限,如果兩個 Flexible 分別為 1 和 2,那麼 Flex 根本不知道該如何將空間劃分成 1:2。如果真這麼做的話,會收到這樣的 error:

...
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
...

小測驗

下面這段代碼會讓 Hello World 被包裹在中間的小方塊里嗎?

return MaterialApp(
  home: Container(
    alignment: Alignment.center,
    constraints: BoxConstraints.tight(Size(100, 100)),
    decoration: BoxDecoration(color: Colors.yellow),
    child: Text('Hello World'),
  ),
);

關於 Flutter Layout 你應該知道的

答案是,不會,它會變成這樣:

關於 Flutter Layout 你應該知道的

不是設置了 constraints 系統就要按着這個 constraints 來,在經過計算之後,系統會發現這個 constraints 無法滿足需求,而被捨棄,具體過程如下:

Containerbuild 方法里,發現有設置過 constraints,最終會返回一個 BoxConstraints :

BoxConstraints(
  minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
  maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
  minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
  maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight)
)

這裡的 clamp 方法指的是當 minWidth 值比左邊的值小時,取左邊值,比右邊的值大時,取右邊值。因為 parent 的 constraints 也就是 screen size 是固定的,因此, minWidth 在跟它們比較之後,還是使用了它們的值。

正確的做法是在外面套一層 CenterAlign widget。

如何得到父 widget 的 constraints?

使用 LayoutBuilder 。有時會需要這些信息來做一些顯示上的調整。

// borrowed from https://stackoverflow.com/a/41558369/94962

var container = Container(
  // Toggling width from 100 to 300 will change what is rendered
  // in the child container
  width: 100.0,
  // width: 300.0
  child: LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
      if(constraints.maxWidth > 200.0) {
        return Text('BIG');
      } else {
        return Text('SMALL');
      }
    }
  ),
);

如何獲取屏幕尺寸

使用 MediaQuery ,除了 size 外,還能拿到 devicePixelRatio 之類的 device 信息。

小結

差不多就這些了,對於理解 Flutter 的布局應該夠用了,希望能帶來些幫助,如果有什麼錯誤歡迎指出 🙂

–EOF–

若無特別說明,本站文章均為原創,轉載請保留鏈接,謝謝

原文 : Limboy

相關閱讀

免责声明:本文内容来源于Limboy,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。