Container — 一個你最熟悉又最陌生的 Widget

更新於 發佈於 閱讀時間約 14 分鐘

每次在開發的時候,碰到不如預期的狀況時,都是一個非常好的機會,可以讓我們更深了解某些事。

最近在開發的時候又碰到一些意料之外的事,經過一些實驗,終於定位了問題點。

讓我們看看以下這段程式碼,在這段程式碼中,我們指定了 Container 的大小為 300 x 300,同時也指定 child 中的 Image 大小為 30 x 30。

Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: Image.asset(
"assets/images/blog.png",
width: 30,
height: 30,
),
)

大家可以在腦海中想像一下,這段程式碼在畫面中會呈現成什麼樣子?是否會覺得下圖這樣呢?

raw-image

但結果卻是 Container 把 Image 也拉大到 300 x 300 了。

raw-image

觀眾們可能會想,都設定圖片大小了,怎麼還是會被放到最大呢?

顯然肯定有個人在搞鬼,今天就來看看這個搞鬼的人:Container

Container 的行為

Container 作為開發 Flutter App 最常用的 Widget 之一,其實有著相當複雜的行為。如果我們看到官方文件,會發現其中有一段文字在描述 Container 的行為。

Container 的佈局行為按以下順序進行:

  • 優先遵循 alignment
  • 根據 child 的大小來決定自身大小。
  • 遵循 widthheight 和 constraints
  • 擴展以適配父級大小。
  • 嘗試盡量小化自身大小。


若是調整一下剛剛的例子,把 width 與 height 拿掉。

Container(
color: Colors.pinkAccent,
child: Image.asset(
"assets/images/blog.png",
width: 30,
height: 30,
),
)

此時就會發現,Container 就遵循了第二條規則:根據 child 大小來決定自身大小

raw-image

設定了 width 與 height 後,到底實際發生了什麼事呢?讓我們深入 Container 的 build 方法一探究竟。

Container 的 build 方法

當我們設定了 width 或 height 而沒有給 constraints 時,實際上 Container 會幫我們生成一個 BoxConstraints.tightFor(width: width, height: height)

Container({
// 省略...
}) : // 省略 ...,
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints;

tighFor 方法會限制 Widget 的大小,指定 Widget 的寬高,那這個 BoxConstraints 會用在哪邊呢?

const BoxConstraints.tightFor({
double? width,
double? height,
}) : minWidth = width ?? 0.0,
maxWidth = width ?? double.infinity,
minHeight = height ?? 0.0,
maxHeight = height ?? double.infinity;

在 build 方法中,我們可以看到剛剛的 constraints 被放在 ConstrainedBox 中,用來限制 Container 的子 Widget。以上面的例子來說,被限制的 Widget 就是放入的 child 的 Image。

@override
Widget build(BuildContext context) {
// Container 的 build 方法
// 省略 ...

if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child: current);
}

// 省略 ...

return current!;
}

所以也就使得了 Image 被拉到與 Container 一樣大小。

使用 alignment

熟悉 Flutter 的開發人員肯定對這狀況也不陌生,知道加上 alignment 參數就能解決問題。

Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
alignment: Alignment.center,
child: Image.asset(
"assets/images/blog.png",
width: 30,
height: 30,
),
)

那為什麼在 Container 中加上 alignment 時,圖片就能維持當初設定的大小呢?讓我們再次看回 Container 的 build 方法中。

@override
Widget build(BuildContext context) {
// Container 的 build 方法
// 省略 ...

if (child == null && (constraints == null || !constraints!.isTight)) {
// 省略 ...
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}

// 省略 ...

if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child: current);
}

// 省略 ...

return current!;
}

當 alignment 不為 null 時,就會在 child 外面包上一層 Align,接著才是在 Align 外面再包上 ConstrainedBox。這樣一來,就使得實際被拉大的是 Align,而非 Image。

如果有認真看 Container 原始碼的觀眾可能會問,即便我沒有設定 alignment,但我有設定 color,而 ConstrainedBox 的下一層 child 應該是 ColoredBox,所以要拉大也是拉大 ColoredBox,而不應該是 Image 吧?

@override
Widget build(BuildContext context) {
// Container 的 build 方法
// 省略 ...

if (child == null && (constraints == null || !constraints!.isTight)) {
// 省略 ...
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}

if (color != null) {
current = ColoredBox(color: color!, child: current);
}

// 省略 ...

if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child: current);
}

// 省略 ...

return current!;
}

的確,ColoredBox 確實會被拉大,但是 ColoredBox 也直接把上頭來的 constraints 直接轉送給了他的 child。在這兩種狀況中,雖然 Image 上層還有其他 Widget,但是卻有不同的結果。

若繼續深入 Align 與 ColoredBox 的佈局方式,很快就有答案了。

一路追蹤 ColoredBox 的原始碼:ColoredBox -> _RenderColoredBox -> RenderProxyBoxWithHitTestBehavior -> RenderProxyBox,最後可以發現 ColoredBox 繼承了 RenderProxyBox。在 RenderProxyBox 的佈局中,其實也就只是把自己收到的限制,直接原封不動的傳給子 Widget,所以即便中間多墊了一層 ColoredBox,也不能避免 Image 被拉大的效果。

@override
void performLayout() {
size = (child?..layout(constraints, parentUsesSize: true))?.size
?? computeSizeForNoChild(constraints);
return;
}

接著看到 Align,Align 繼承了 RenderPositionedBox。在 RenderPositionedBox 的佈局中,我們可以發現,它從上頭接收到了限制,接著轉頭就將限制放寬,讓子 Widget 可以挑選他希望的大小。所以在 Align 中,Image 可以維持當初設定的 30 x 30 的大小。

@override
void performLayout() {
final BoxConstraints constraints = this.constraints;

// 省略 ...

if (child != null) {
child!.layout(constraints.loosen(), parentUsesSize: true);

// 省略 ...

} else {...}
}

實驗放入不同的 Widget

最後讓我們做一些實驗,如果採用相同 Container 設定,但是在 child 中放入不同東西,看看會發生什麼事?

放入指定大小的 Container

與 Image 一樣,放入了指定大小的 Container,結果這個 Container 還是被拉大到 300 x 300。

Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: Container(
width: 100,
height: 100,
color: Colors.blueAccent,
),
)
raw-image

放入 TextButton

乍看之下,放入的 TextButton 好像沒被拉大,但實際上卻是有的,我們可以從 Hover 效果看出,按鈕還是被拉大了。

Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: TextButton(
onPressed: () {},
child: const Text('Click me'),
),
)
raw-image

放入 Text

當我們試到 Text 的時候,卻發現 Text 好像就沒被拉大的問題,這又是怎麼一回事呢?

Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: const Text("Hello World"),
)
raw-image

關於這個問題,有機會再讓我們深入探討,好奇的觀眾也可以先自行研究看看。

小結

Container 做為我們最常使用的 Widget 之一,了解他如何運作對於開發必然有些幫助。雖然不是每天都會碰到 Widget 排版不如預期的問題,但是每次碰上就會相當困擾,需要花許多時間嘗試才能解決。

追蹤原始碼,了解 Widget 底層的運作邏輯,能夠提供我們更多解決問題的思路。當未來碰上問題時,就能用正確又快速的方式解決,而不是留下更多的 Workaround。






分享各種軟體開發技巧與心得
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
你可能也想看
Google News 追蹤
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~