前言
在浏览本文之前咱们先来回顾下在 Flutter 开发过程中,是不是常常会遇到以下问题:
- Container 设置了宽高有效
- Column 溢出边界,Row 溢出边界
- 什么时候该应用 ConstrainedBox 和 UnconstrainedBox
每当遇到这种问题,我总是一直地尝试,费了九牛二虎之力,Widget 终于乖乖就范(达到现实成果)。痛定思过,我终于开始镇压(起来,不愿做奴隶的人们,国歌唱起来~),为什么 Container 设置宽高又有效了?Column 为什么又溢出边界了?怀揣着满腔热血,我终于鼓起勇气首先从 Container 源码动手,逐个揭开它的神秘面纱。
布局规定
在讲本文之前,咱们首先应该理解 Flutter 布局中的以下规定:
- 首先,下层 Widget 向上层 Widget 传递约束条件
- 其次,上层 Widget 向下层 Widget 传递大小信息
- 最初,下层 Widget 决定上层 Widget 的地位
如果咱们在开发时无奈纯熟使用这些规定,在布局时就不能齐全了解其原理,所以越早把握这些规定越好。
- Widget 会通过它的父级取得本身束缚。束缚实际上就是 4 个浮点类型的汇合:最大 / 最小宽度,以及最大 / 最小高度。
- 而后这个 Widget 将会一一遍历它的 children 列表,向子级传递束缚(子级之间的束缚可能会有不同),而后询问它的每一个子级须要用于布局的大小。
- 而后这个 Widget 将会对它子级 children 一一进行布局。
- 最初,Widget 将会把它的大小信息向上传递至父 Widget(包含其原始约束条件)。
严格束缚(Tight)vs. 宽松束缚(Loose)
严格束缚就是取得确切大小的抉择,换句话来说,它的最大 / 最小宽度是统一的,高度也是一样。
// flutter/lib/src/rendering/box.dart
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
宽松束缚就是设置了最大宽度 / 高度,然而容许其子 Widget 取得比它更小的任意大小,换句话说就是宽松束缚的最小宽度 / 高度为 0。
// flutter/lib/src/rendering/box.dart
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
Container 局部源码
首先奉上 Container 局部源码,上面咱们会联合具体场景对源码进行逐个剖析。
// flutter/lib/src/widgets/container.dart
class Container extends StatelessWidget {
Container({
Key key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child,
this.clipBehavior = Clip.none,
}) : assert(margin == null || margin.isNonNegative),
assert(padding == null || padding.isNonNegative),
assert(decoration == null || decoration.debugAssertIsValid()),
assert(constraints == null || constraints.debugAssertIsValid()),
assert(clipBehavior != null),
assert(
color == null || decoration == null,
'Cannot provide both a color and a decoration\n'
'To provide both, use"decoration: BoxDecoration(color: color)".'),
constraints = (width != null || height != null)
? constraints?.tighten(width: width, height: height) ??
BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);
final Widget child;
// child 元素在 Container 中的对齐形式
final AlignmentGeometry alignment;
// 填充内边距
final EdgeInsetsGeometry padding;
// 色彩
final Color color;
// 背景装璜
final Decoration decoration;
// 前景装璜
final Decoration foregroundDecoration;
// 布局束缚
final BoxConstraints constraints;
// 外边距
final EdgeInsetsGeometry margin;
// 绘制容器之前要利用的变换矩阵
final Matrix4 transform;
// decoration 参数具备 clipPath 时的剪辑行为
final Clip clipBehavior;
EdgeInsetsGeometry get _paddingIncludingDecoration {if (decoration == null || decoration.padding == null) return padding;
final EdgeInsetsGeometry decorationPadding = decoration.padding;
if (padding == null) return decorationPadding;
return padding.add(decorationPadding);
}
@override
Widget build(BuildContext context) {
Widget current = child;
if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
if (alignment != null)
current = Align(alignment: alignment, child: current);
final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (color != null) current = ColoredBox(color: color, child: current);
if (decoration != null)
current = DecoratedBox(decoration: decoration, child: current);
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration,
position: DecorationPosition.foreground,
child: current,
);
}
if (constraints != null)
current = ConstrainedBox(constraints: constraints, child: current);
if (margin != null) current = Padding(padding: margin, child: current);
if (transform != null)
current = Transform(transform: transform, child: current);
if (clipBehavior != Clip.none) {
current = ClipPath(
clipper: _DecorationClipper(textDirection: Directionality.of(context), decoration: decoration),
clipBehavior: clipBehavior,
child: current,
);
}
return current;
}
}
场景剖析
场景一
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Container(color: Colors.red,),
),
在 Scaffold body 中独自应用 Container,并且 Container 设置 color 为 Colors.red。
关上 DevTools 进行元素查看咱们能够发现 Widget Tree 的构造 Container -> ColoredBox -> LimitedBox -> ConstrainedBox,最初会创立 RenderConstrainedBox,宽度和高度撑满整个屏幕(除了 AppBar)。
那咱们不禁会问,为什么会这样,我并没有设置 Container 的宽度和高度,那么咱们再次回到下面的源码,如果 Container 没有设置 child 参数并且满足 constraints == null || !constraints.isTight
会返回一个 maxWidth 为 0,maxHeight 为 0 的 LimitedBox 的元素,并且 LimitedBox 的 child 是一个 constraints 参数为 const BoxConstraints.expand()
的 ConstrainedBox 的元素,所以 Container 会撑满整个屏幕(除了 AppBar)。
// flutter/lib/src/widgets/container.dart
if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
// flutter/lib/src/rendering/box.dart
const BoxConstraints.expand({
double width,
double height,
}) : minWidth = width ?? double.infinity,
maxWidth = width ?? double.infinity,
minHeight = height ?? double.infinity,
maxHeight = height ?? double.infinity;
场景二
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
在场景一的根底上进行批改,此时给 Container 设置 width 为 100,height 为 100,color 为 Colors.red。
同样关上 DevTools 进行元素查看咱们能够发现 Widget Tree 的构造 Container -> ConstrainedBox -> ColorededBox,最初会创立 _RenderColoredBox,宽度和高度均为 100,色彩为红色的正方形。
通过源码剖析咱们能够得出,如果 Container 中设置了 width、height 并且没有设置 constraints 属性,首先会在构造函数中对 constraints 进行赋值,所以 constraints = BoxConstraints.tightFor(width:100, height:100)
,而后会在外层嵌套一个 ColoredBox,最初再嵌套一个 ConstrainedBox 返回。
Container({
Key key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child,
this.clipBehavior = Clip.none,
}) : assert(margin == null || margin.isNonNegative),
assert(padding == null || padding.isNonNegative),
assert(decoration == null || decoration.debugAssertIsValid()),
assert(constraints == null || constraints.debugAssertIsValid()),
assert(clipBehavior != null),
assert(color == null || decoration == null,
'Cannot provide both a color and a decoration\n'
'To provide both, use"decoration: BoxDecoration(color: color)".'
),
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);
if (decoration != null)
current = DecoratedBox(decoration: decoration, child: current);
if (constraints != null)
current = ConstrainedBox(constraints: constraints, child: current);
场景三
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Container(
width: 100,
height: 100,
color: Colors.red,
alignment: Alignment.center,
),
),
接下来,咱们在场景二的根底上持续增加 alignment:Alignment.center
属性。
此时咱们会发现为什么没有居中显示呢?通过查看 Align 源码不难发现,它是设置子 Widget 与本身的对齐形式。
A widget that aligns its child within itself and optionally sizes itself based on the child’s size.
那么此时咱们再来扭转代码,给以后 Container 增加子 Widget,终于达到了咱们想要的居中成果。
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Container(
width: 100,
height: 100,
color: Colors.red,
alignment: Alignment.center,
child: Container(
width: 10,
height: 10,
color: Colors.blue,
),
),
),
场景四
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Center(
child: Container(
color: Colors.red,
width: 200,
),
),
),
因为 Scaffold 中的 body 元素会撑满整个屏幕(除了 AppBar),body 通知 Center 占满整个屏幕,而后 Center 通知 Container 能够变成任意大小,然而 Container 设置 width 为 200,所以 Container 的大小为宽度 200,高度无限大。
The primary content of the scaffold.
Displayed below the [appBar], above the bottom of the ambient
场景五
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Center(
child: Row(
children: <Widget>[
Container(
color: Colors.red,
child: Text(
'我是一段很长很长很长的文字',
style: TextStyle(fontSize: 30,),
),
),
Container(
color: Colors.red,
child: Text('我是一段很短的文字',),
),
],
),
),
),
因为 Row 不会对其子元素施加任何束缚,因而它的 children 很有可能太大而超出 Row 的宽度,在这种状况下,Row 就会显示出溢出正告了。
场景六
Scaffold(
appBar: AppBar(title: Text('Flutter Container'),
),
body: Center(
child: Container(
constraints: BoxConstraints(
maxHeight: 400,
minHeight: 300,
minWidth: 300,
maxWidth: 400,
),
color: Colors.red,
width: 200,
),
),
),
这里咱们设置了 Container 的 constraints 属性值为 BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:400),
并且设置了 width 为 200。所以在构造函数初始化参数时,会进行设置 constraints = BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:300)
, 在 Container build 函数中会返回一个这样的 Widget Tree 的构造(Container -> ConstrainedBox -> ColoredBox -> LimitedBox -> ConstrainedBox)。
此时 Center 通知 Container 能够变成任意大小,然而 Container 设置 constraints 约束条件为宽度最小为 300,最大为 300,也就是宽度为 300,最小高度为 300,最大高度为 400,所以在 Container 中设置的 width 为 200 也就有效了,这个时候你兴许会问,那高度到底是多少?答案是 400,因为 Container 中没有设置 child,满足 child == null && (constraints == null || !constraints.isTight)
条件,所以会嵌套一个 ConstrainedBox(constraints: const BoxConstraints.expand()
所以高度会为最大高度 400。
// flutter/lib/src/rendering/box.dart
BoxConstraints tighten({double width, double height}) {
return BoxConstraints(minWidth: width == null ? minWidth : width.clamp(minWidth, maxWidth) as double,
maxWidth: width == null ? maxWidth : width.clamp(minWidth, maxWidth) as double,
minHeight: height == null ? minHeight : height.clamp(minHeight, maxHeight) as double,
maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight) as double,
);
}
// flutter/lib/src/rendering/box.dart
/// Whether there is exactly one width value that satisfies the constraints.
bool get hasTightWidth => minWidth >= maxWidth;
/// Whether there is exactly one height value that satisfies the constraints.
bool get hasTightHeight => minHeight >= maxHeight;
/// Whether there is exactly one size that satisfies the constraints.
@override
bool get isTight => hasTightWidth && hasTightHeight;
// flutter/lib/src/widgets/container.dart
if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
最初
通过以上源码剖析以及不同的场景,咱们不难发现 Container 次要就是通过设置不同的参数,而后应用 LimitedBox、ConstrainedBox、Align、Padding、ColoredBox、DecoratedBox、Transform、ClipPath 等 Widget 进行组合而来。