提出问题
在书写 flutter 时是否偶尔发现自己写的宽高没有起作用?其中可能产生的原因就是与 flutter 的默认的约束规则产生了抵触。
demo1
在 flutter 中,如果书写代码
Container(
width: 100,
height: 100,
color: Colors.purple,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
);
会得到什么呢?我的第一反应就是会得到分别得到一个紫色大方块与蓝色小方块。但是实际上经过 devtool 查看,父子宽高均为100,最终只看到一个蓝色的宽高100的方块。看上去似乎是父组件约束了子组件的宽高。
由此我们知道,父组件可以影响子组件的尺寸。
demo2
Container(
color: Colors.purple,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
);
这个就比较简单了,我们会得到一个蓝色的宽高50的方块。那么外层 Container 的尺寸是多少呢?通过 flutter devtools 的 flutter inspetor 我们可以知道外层 Container 的尺寸也是 50。
由此我们知道,子组件也可以反过来影响父组件的尺寸。
疑问小结
我们发现2个知识点看上去是有冲突的,假设有组件嵌套关系如:A-B-C,A 要影响 B 的尺寸,C 也要影响 B 的尺寸,那么到底最终谁说了算?其源码的实现思路又是怎样的呢?
Container(
width: 100,
height: 100,
color: Colors.purple,
child: Container(
color: Colors.yellow,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
);
中间的 Container 尺寸究竟会是多少呢?
源码解析
Container 的真身
Container 其实是 flutter 中最常见的组件了,但是其实它是一个复合组件。稍微看一下源码就知道了,如下:
// Container
@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 (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.of(context),
decoration: decoration
),
clipBehavior: clipBehavior,
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);
return current;
}
Container 根据构造函数传入的属性,build 时返回了若干个嵌套的组件。这里我们主要关注处理宽高的地方,查看构造函数我们知道 width , height 属性都被写入 constraints 属性了。而 build 里 constraints != null 就会被丢入 ConstrainedBox 组件。
前置知识——Widget Element RenderObject 的关系
太过庞大,这里不做展开。(或者在某个时空我会补上?)
ConstrainedBox 的真身
查看 ConstrainedBox 的继承链,我们可以发现它继承于 RenderObjectWidget,它是没有 build 方法的,会有一个 renderObject 决定该组件如何绘制。
我们重点关注它覆写的 createRenderObject 方法
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(additionalConstraints: constraints); // constraints 在构造函数初始化了
}
带领我们来到 RenderConstrainedBox,观察其继承链。
其继承于 RenderObject ,如果你阅读过其源码,你会发现注释里面有这么一句话
Subclasses should not override [layout] directly. Instead, they should override [performResize] and/or [performLayout]. The [layout] method delegates the actual work to [performResize] and [performLayout].
RenderConstrainedBox 没有覆写 performResize, 只覆写了 performLayout。那么我们看看其代码实现。
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
// 根据上方我们的 demo, 可知 child 不会为 null
if (child != null) {
// _additionalConstraints 在构造函数中初始化
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
// 使用子组件的尺寸
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
其中 _additionalConstraints.enforce(constraints) 的实现如下
BoxConstraints enforce(BoxConstraints constraints) {
return BoxConstraints(
// clamp 是 num 类的一个方法,译为 钳
// 作用是将当前数值钳在一个范围内,比如
// 5.clamp(1, 7) == 5; 5.clamp(10, 20) == 10
minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
);
}
其目的也很好理解了,就是让自身已经存在的 constraints (父级给的)约束住 _additionalConstraints(通过构造函数传入的)。由此我们找到了第一个 demo 的实现,让我们先贴一遍 demo1 的代码。
Container(
width: 100,
height: 100,
color: Colors.purple,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
);
child.layout 此处不做过多解析。
子级设置的宽高优先级低于父级给到的宽高,因为会被调用 boxConstraints.enfore 钳住。父组件可以影响子组件的尺寸
别忘了调用完 child.layout 后我们还有一句 size = child!.size;,这也符合我们的第二个规则 子组件也可以反过来影响父组件的尺寸
但是第一规则优先级是大于第二规则的,所以有了 demo2 最终的结果——三个 Container 其宽高都为 100。
打破约束
如果基于 demo1 我们就是想要一个宽高100的父级嵌套宽高50的子级怎么办?官方也给我们提供了解决方案,只要给子级嵌套一个 UnconstrainedBox 组件就好了,至于具体的源码,各位可以自己尝试去看看。只要找到其埋藏于底层的 performLayout 就很容易理解了。
总结
flutter 中不少组件隐式的约束了子组件的宽高,比如 PageView,如果发现自己设置的宽高无效,记得使用 UnconstrainedBox。
其背后蕴含的原理就是 flutter 的约束规则(实际上有很多种约束规则,本文只讲了最常见的一种)。