0. Container 的簡介
如果你看過
Container
的源碼,會發現它是一個很有意思的元件,它基本上沒幹啥正事,就是将已有的元件拼一拼而已。它是一個
StatelessWidget
,其中
build
方法使用了如下八個元件,本文将從源碼的角度看一下,Container 到底是如何運作的,為其設定的各種屬性都被用在了哪裡。
1. 顔色屬性
在
LayoutBuilder
篇我們知道,Scaffold 元件的 body 對應上層的區域限制為
BoxConstraints(0.0<=w<=螢幕寬, 0.0<=h<=螢幕高)
。從表現上來看,當隻有 color 屬性時,Container 的尺寸會鋪滿最大限制區域。
[email protected]
2Widget build(BuildContext context) {
3 Widget current = child;
4 if (child == null && (constraints == null || !constraints.isTight)) {
5 current = LimitedBox(
6 maxWidth: 0.0,
7 maxHeight: 0.0,
8 child: ConstrainedBox(constraints: const BoxConstraints.expand()),
9 );
10 }
11 ...
12 if (color != null)
13 current = ColoredBox(color: color, child: current);
14 ...
15 return current;
16}
從代碼中可以看到,當 child 為 null ,并且 constraints 為 null,
current
會被套上 LimitedBox + ConstrainedBox,其中 ConstrainedBox 的限制是延展的。當顔色非 null,會在
current
上傳套上
ColoredBox
,而
ColoredBox
元件的作用就是在尺寸區域中填充顔色。這就是 Container 的尺寸會鋪滿最大限制區域的原因。
2. child 屬性
如下,可見當設定
child
屬性後,
Container
的布局尺寸會與
child
一緻。來看下源碼這是為什麼。
通過上面的源碼也可以看出, 當 child 屬性非空時,就不會包裹
LimitedBox
+
ConstrainedBox
。從下面的調試結構看,隻有
ColoredBox
+
Text
。是以就沒有了區域的延展,進而和
child
尺寸一緻。
3. 寬高屬性
添加寬高屬性之後,
Container
的布局區域會變為指定區域。那源碼中是如何實作的呢?
1 Container({
2 //...
3 double width,
4 double height,
5 //...
6 }) : //...
7 constraints =
8 (width != null || height != null)
9 ? constraints?.tighten(width: width, height: height)
10 ?? BoxConstraints.tightFor(width: width, height: height)
當寬高被設定時,
constraints
屬性會被設定為對應寬高的緊限制,也就是把尺寸定死。
4.alignment 屬性
通過 alignment 可以将子元件在容器區域内對齊擺放。那源碼中是如何實作的呢?
1if (alignment != null)
2 current = Align(alignment: alignment, child: current);
其實處理非常簡單,就是在
alignment
非空時,套上一個
Align
元件。
5.padding 和 margin 屬性
通過布局檢視器可以看出,外邊距是
margin
,内邊距是
padding
。
1EdgeInsetsGeometry get _paddingIncludingDecoration {
2 if (decoration == null || decoration.padding == null)
3 return padding;
4 final EdgeInsetsGeometry decorationPadding = decoration.padding;
5 if (padding == null)
6 return decorationPadding;
7 return padding.add(decorationPadding);
8}
[email protected]
10Widget build(BuildContext context) {
11 Widget current = child;
12 //...
13 final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
14 if (effectivePadding != null)
15 current = Padding(padding: effectivePadding, child: current);
16 //...
17 if (margin != null)
18 current = Padding(padding: margin, child: current);
19 return current;
20}
從源碼中可以看出
padding
和
margin
屬性都是使用
Padding
屬性完成的,隻不過
margin
在外側包裹而已。可以看到實際的
padding
值是通過
_paddingIncludingDecoration
獲得的,其中會包含裝飾的邊距,預設為 0。
6.decoration 和 foregroundDecoration 屬性
decoration 屬性和 foregroundDecoration 非空時,都會包裹一個
DecoratedBox
元件。foregroundDecoration 是前景裝飾,是以較背景裝飾而言在上層。關于
DecoratedBox
元件的使用在之前介紹過,這裡就不再詳細介紹了,可詳見之前的
DecoratedBox
元件文章。
1if (decoration != null)
2 current = DecoratedBox(decoration: decoration, child: current);
3
4if (foregroundDecoration != null) {
5 current = DecoratedBox(
6 decoration: foregroundDecoration,
7 position: DecorationPosition.foreground,
8 child: current,
9 );
10}
7. constraints 屬性
constraints
屬性非空,會包裹上
ConstrainedBox
,此時容器的區域會被限制,如下測試中,限制為最小寬高 80、32,最大寬高 100,140。即說明目前容器的所占區域不能在限制之外,這裡寬高為 8,比最小區域寬高小,則會使用最小寬高。
當設定大小比限制區域大時,會使用最大的限制區域,也就是說如果目前容器的布局區域發生變化,
constraints
會保證容器尺寸在一個範圍内變化。比如盛放文字時,文字的長短不同導緻布局尺寸不同,通過限制可以讓文字在一定的尺寸範圍内變動。
1if (constraints != null)
2 current = ConstrainedBox(constraints: constraints, child: current);
8. clipBehavior 裁剪行為
Clip
是一個枚舉類,包含四種形式,如下:
1enum Clip {
2 none, // 無
3 hardEdge, // 硬邊緣
4 antiAlias, // 抗鋸齒
5 antiAliasWithSaveLayer, // 抗鋸齒儲存圖層
6}
從源碼中可以看出
clipBehavior
不為
Clip.none
時,必須有
decoration
屬性。這裡将
current
包裹一層
ClipPath
,
clipBehavior
就是在該元件中使用的。這裡的裁剪使用
_DecorationClipper
,通過
decoration
擷取裁剪路徑,也就是圓角裝飾時的裁剪行為。
1if (clipBehavior != Clip.none) {
2 assert(decoration != null);
3 current = ClipPath(
4 clipper: _DecorationClipper(
5 textDirection: Directionality.of(context),
6 decoration: decoration
7 ),
8 clipBehavior: clipBehavior,
9 child: current,
10 );
11}
12
13/// A clipper that uses [Decoration.getClipPath] to clip.
14class _DecorationClipper extends CustomClipper<Path> {
15 _DecorationClipper({
16 TextDirection textDirection,
17 @required this.decoration
18 }) : assert(decoration != null),
19 textDirection = textDirection ?? TextDirection.ltr;
20
21 final TextDirection textDirection;
22 final Decoration decoration;
23
24 @override
25 Path getClip(Size size) {
26 return decoration.getClipPath(Offset.zero & size, textDirection);
27 }
28
29 @override
30 bool shouldReclip(_DecorationClipper oldClipper) {
31 return oldClipper.decoration != decoration
32 || oldClipper.textDirection != textDirection;
33 }
34}
9. transform 屬性
transform 接收一個
Matrix4
的變化矩陣對象,可以據此完成一些移動、旋轉、縮放的變換效果。不過通過源碼可以看出
Container
元件隻是對
Transform
的一個簡單封裝,實際上
Transform
還可以指定
變化中心 origin
、
對齊模式 alignment
等。這樣可以看出來
Container
隻是為了元件的簡化使用,并非全權将這些元件的功能進行內建。
1if (transform != null)
2 current = Transform(transform: transform, child: current);
10. Container 元件存在的意義
對于這些
SingleChildRenderObjectWidget
,由于各自的屬性比較少,有些功能很常用,當聯合使用時,就會一層層嵌套,導緻使用的體驗不是很好。如果沒有
Container
元件,那麼要完成上面的效果,你就需要使用下面右側的實作方式,将這些小元件一個個嵌套,這樣用起來是非常麻煩和别扭的。
當有了
Container
,雖然它沒有幹什麼非常偉大的事,卻實實在在地将這八個元件整合到了一起。如右側圖檔,使用起來就非常精簡。但本質上還是那些元件的功勞,這就是一種封裝,将多個子系統内聚,對外界提供通路的接口,表面上操作的是外表的接口,實際上是子系統的運作。
Container 是一個 StatelessWidget,它隻需要完成
build
的任務,依賴其他元件來完成任務,這是一件比較輕松的事。通過設定
Container
元件的屬性,再将這些屬性移交給内部的各個元件,可以很有效地
表象的樹狀結構拉平
,這樣的好處是提供代碼的
易讀性
,通過
Container
的元件名,也有一定的
語義性
。更友善使用者的了解和使用。我們再反過來思考一下,源碼中可以這樣,如果有類似的場景,很多短小的層級結構,我們也可以适當地封裝一個元件進行優化。
從源碼可以看出對于 Align 、Transform 元件,Container 并沒有将它們全部屬性都內建進來。這樣看來
Container
隻是一個通才,什麼都能幹,但并不需要樣樣都精。如果暴露了過多的屬性,會增加使用者使用的複雜性。是以凡事适度,才能有最好的效果。
最後說一下,通過源碼分析後,我們應該可以明白,有些很簡單的場景是不需要使用
Container
的,比如隻是為了加個
Padding
、隻是顯示一下顔色、隻是進行變換等,使用對應的元件即可。當需要同時使用幾個功能時,使用 Container 時也不必有什麼負擔,擔心使用 Container 低效什麼的,其實就是在元素樹裡多了個元素而已,代碼可讀性的價值遠遠在其之上,自己一層層疊也可能是
多寫多錯
。了解
Container
的源碼之後,在使用時便不再陌生,一個黑盒被照亮後,在使用它的時候,你就會多一份自信。這篇就到這,謝謝觀看。