这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战
往期精彩
👉 Flutter 必知必会系列—— Element 的更新复用机制
👉 Flutter 必知必会系列 —— Element 更新实战
👉 Flutter 必知必会系列 —— Render 树的布局绘制
之前的文章中,我们知道了 Widget、Element 和 RenderObject 的分工,以及 RenderObject 布局的理论。尤其是 👉 深入理解Flutter布局约束 总结的:向下👇传递约束。向上👆传递尺寸
理论就要从客观实际中抽出来,又在客观实际中充分证明。
这一篇文章我们就从代码的角度,验证 👉 深入理解Flutter布局约束 中的两个小例子。
案例分析
案例一:最小还是最大
案例代码就是 深入理解Flutter布局约束的案例一
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return RedContainer();
}
}
class RedContainer extends StatelessWidget {
const RedContainer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
);
}
}
我们在页面上直接返回了红色背景的 Container。没有设置宽高。大家已经知道了会整个屏幕都是红色,而不是啥也不显示。
理由呢就是:
屏幕是
Container
的父节点,屏幕强制Container
的大小和screen相同。
因此,Container
使用红色充满了整个屏幕。
下面我们就代码的角度,来看整个过程。
我们是在屏幕上直接显示了 RedContainer
,相当于 Flutter 的根节点的直接子节点就是
RedContainer
,所以 红色 Container 的盒子约束就是:
const BoxConstraints({
this.minWidth = 屏幕宽度,
this.maxWidth = 屏幕宽度,
this.minHeight = 屏幕高度,
this.maxHeight = 屏幕高度,
});
虽然代码写的是 Container( color: Colors.red, ) ,但是实际构造的是 ColoredBox
:
构造的节点如下:
ColoredBox 设置了红色背景,LimitedBox 和 ConstrainedBox 设置了布局宽高。
约束是层层传递的,我们从 ColoredBox 到 ConstrainedBox 依次追下来。
下面就是 ColoredBox
对应的 _RenderColoredBox
的布局过程:
@override
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
我们看到 ColoredBox 没参与布局过程,并且红色区域的宽高还是子节点 LimitedBox 的宽高。
布局的时候直接调用了子节点的布局过程,约束还是从父节点透传的。
这里入参的 constraints 就是页面约束屏幕宽高
minWidth:就是屏幕宽度
maxWidth:constraints.hasBoundedWidth是true,所以也是屏幕宽度
minHeight:就是屏幕高度
maxHeight:和最大宽度相似,是屏幕高度/
下面我们在追 LimitedBox 的布局过程。构造的 LimitedBox 如下:
if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
LimitedBox 的最大宽度 和 最大高度都是 0,子节点是 ConstrainedBox 。
LimitedBox 创建的 RenderBox 是:RenderLimitedBox
下面,我们看 RenderLimitedBox 的布局过程(performLayout):
@override
void performLayout() {
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild,
);
}
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild }) {
if (child != null) {
final Size childSize = layoutChild(child!, _limitConstraints(constraints));
return constraints.constrain(childSize);
}
return _limitConstraints(constraints).constrain(Size.zero);
}
我们看到通过 _computeSize 确定了尺寸,_computeSize 方法的 constraints 约束就是 ColoredBox
传进来的约束 ———— 屏幕宽高。
在实际的 _computeSize 方法中,我们走到了 child != null 的判断。
虽然 Container 的 child 是 null,但是构造的 LimitedBox 是有子节点的。
这里我们看到,它对原始的 constraints 进行了限制计算(_limitConstraints)。并将计算的结果,作为子节点的约束。
重点就是计算:
BoxConstraints _limitConstraints(BoxConstraints constraints) {
return BoxConstraints(
minWidth: constraints.minWidth,
maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth),
minHeight: constraints.minHeight,
maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : constraints.constrainHeight(maxHeight),
);
}
这里的入参 constraints 还记得是谁么,就是 ColoredBox 的约束 —— 屏幕宽高
生成的值如下:
minWidth:就是屏幕宽度
maxWidth:constraints.hasBoundedWidth 是 true,所以也是屏幕宽度
minHeight:就是屏幕高度
maxHeight:和最大宽度相似,是屏幕高度/
也就是说,生成的子节点约束就是屏幕宽高
同样的道理,我们再追到 RenderConstrainedBox 中看布局测量。
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
同样,这里的 constraints 就是父节点传进来的约束 ———— 屏幕宽高。
RenderConstrainedBox 下面没有子节点。
所以尺寸就是 _additionalConstraints.enforce(constraints).constrain(Size.zero) 的结果。
_additionalConstraints
是谁呢?就是 Container 中 build 传递的。
_additionalConstraints
如下:
const BoxConstraints({
this.minWidth = double.infinity,
this.maxWidth = double.infinity,
this.minHeight = double.infinity,
this.maxHeight = double.infinity,
});
enforce 计算过程如下:
BoxConstraints enforce(BoxConstraints constraints) {
return BoxConstraints(
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),
);
}
大家可能不熟悉 clamp 方法,clamp 是一个数学方法:在一个合理的范围取值,要么是数值本身,要么是两端。
如果数值,在两端之间,那 clamp 的结果就是数值本身。如果小于左断点,那就是左端点的值,如果大于右端点,那就是右端点的值。这就是夹计算。
所以 enforce 的结果是:
const BoxConstraints({
this.minWidth = 屏幕宽度,
this.maxWidth = 屏幕宽度,
this.minHeight = 屏幕高度,
this.maxHeight = 屏幕高度,
});
我们在看 constrain
计算过程:
Size constrain(Size size) {
Size result = Size(constrainWidth(size.width), constrainHeight(size.height));
return result;
}
double constrainWidth([ double width = double.infinity ]) {
return width.clamp(minWidth, maxWidth);
}
宽度是 屏幕宽度.clamp(0,无穷)
高度是 屏幕高度.clamp(0,无穷)
到此就确定了尺寸,屏幕宽度。并且将尺寸层层上传。
所以 整个屏幕显示红色的屏幕
整个过程就验证了:约束向下传递,尺寸向上传递
。
案例二:能够显示指定尺寸吗
我们现在改变一下:指定一下宽高。
class RedContainer extends StatelessWidget {
const RedContainer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
width:100,
height:100,
);
}
}
显示的也是整个屏幕,因为 父节点强制显示整个屏幕,下面我们就看强制的过程。
这里我们先看一下 Container 构造方法:
Container({
Key? key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior = Clip.none,
}) : constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);
因为我们设置了 width 和 height,所以就会在构造方法中 tightFor Constraints 。构造的结果是:
const BoxConstraints({
this.minWidth = 100,
this.maxWidth = 100,
this.minHeight = 100,
this.maxHeight = 100,
});
根据 build 方法的逻辑,就会构造出以下节点
ColorBox 我们就不说了,重点是 ConstrainedBox
。
我们就看对应的 RenderConstrainedBox 的布局逻辑。
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
_additionalConstraints
就是 100 * 100。
constraints
就是父节点传递的约束 屏幕宽高。
enforce
的结果依然是屏幕宽高。
BoxConstraints enforce(BoxConstraints constraints) {
return BoxConstraints(
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),
);
}
minWidth 是 100
constraints.minWidth 是 屏幕宽度(大于100)
constraints.maxWidth 是 屏幕宽度(大于100)
所以结果的 minWidth 是 屏幕宽度,其他的同理。因此,布局 ColorBox 的约束就是屏幕宽高。
有了案例一的铺垫,显示的结果还是整个屏幕就是红色。
整个过程就验证了:父节点强制子节点是多大的
。就是文档中的一句话 When a widget tells its child that it must be of a certain size, we say the widget supplies tight constraints to its child。
总结
上面我们通过两个个案例,来一步一步追了布局的流程,知道了怎么追一个组件的布局。结论不重要,知道怎么追才是最重要的。
大家只需要按着下面的步骤就可以:
第一:通过Widget,找到渲染对象
第二:找到渲染对象的 performLayout 和 performResize