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
的源码之后,在使用时便不再陌生,一个黑盒被照亮后,在使用它的时候,你就会多一份自信。这篇就到这,谢谢观看。