每次在開發的時候,碰到不如預期的狀況時,都是一個非常好的機會,可以讓我們更深了解某些事。
最近在開發的時候又碰到一些意料之外的事,經過一些實驗,終於定位了問題點。
讓我們看看以下這段程式碼,在這段程式碼中,我們指定了 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,
),
)
大家可以在腦海中想像一下,這段程式碼在畫面中會呈現成什麼樣子?是否會覺得下圖這樣呢?
但結果卻是 Container 把 Image 也拉大到 300 x 300 了。
觀眾們可能會想,都設定圖片大小了,怎麼還是會被放到最大呢?
顯然肯定有個人在搞鬼,今天就來看看這個搞鬼的人:Container。
Container 作為開發 Flutter App 最常用的 Widget 之一,其實有著相當複雜的行為。如果我們看到官方文件,會發現其中有一段文字在描述 Container 的行為。
Container 的佈局行為按以下順序進行:
alignment
。width
、height
和 constraints
若是調整一下剛剛的例子,把 width
與 height
拿掉。
Container(
color: Colors.pinkAccent,
child: Image.asset(
"assets/images/blog.png",
width: 30,
height: 30,
),
)
此時就會發現,Container 就遵循了第二條規則:根據 child 大小來決定自身大小。
設定了 width
與 height
後,到底實際發生了什麼事呢?讓我們深入 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 一樣大小。
熟悉 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 {...}
}
最後讓我們做一些實驗,如果採用相同 Container 設定,但是在 child 中放入不同東西,看看會發生什麼事?
與 Image 一樣,放入了指定大小的 Container,結果這個 Container 還是被拉大到 300 x 300。
Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: Container(
width: 100,
height: 100,
color: Colors.blueAccent,
),
)
乍看之下,放入的 TextButton 好像沒被拉大,但實際上卻是有的,我們可以從 Hover 效果看出,按鈕還是被拉大了。
Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: TextButton(
onPressed: () {},
child: const Text('Click me'),
),
)
當我們試到 Text 的時候,卻發現 Text 好像就沒被拉大的問題,這又是怎麼一回事呢?
Container(
width: 300,
height: 300,
color: Colors.pinkAccent,
child: const Text("Hello World"),
)
關於這個問題,有機會再讓我們深入探討,好奇的觀眾也可以先自行研究看看。
Container 做為我們最常使用的 Widget 之一,了解他如何運作對於開發必然有些幫助。雖然不是每天都會碰到 Widget 排版不如預期的問題,但是每次碰上就會相當困擾,需要花許多時間嘗試才能解決。
追蹤原始碼,了解 Widget 底層的運作邏輯,能夠提供我們更多解決問題的思路。當未來碰上問題時,就能用正確又快速的方式解決,而不是留下更多的 Workaround。