根据前文我们已经从宏观上得知:Layout流程的本质是父节点向子节点传递自己的布局约束Constraints,子节点计算自身的大小(Size),父节点再根据大小信息计算偏移(Offset)。在二维空间中,根据大小和偏移可以唯一确定子节点的位置。
Flutter中主要存在两种布局约束——Box
和Sliver
,关键类及其关系如图6-1所示。
图6-1中,BoxConstraints
和SliverConstraints
分别对应Box
布局和Sliver
布局模型所需要的约束条件。ParentData
是RenderObject
所持有的一个字段,用于为父节点提供额外的信息,比如RenderBox
通过BoxParentData
向父节点暴露自身的偏移值,以用于Layout
阶段更新和Paint
阶段。Sliver
通过SliverGeometry
描述自身的Layout
结果,相对Box
更加复杂。
Box布局模型
Box
类型的Constraints
布局在移动UI框架中非常普遍,比如Android的ConstraintLayout
和iOS的AutoLayout
都有其影子。Constraints
布局的特点是灵活且高效。Flutter中Box
布局的原理如图6-2所示。
下面主要介绍 Box
布局模型中最常见的两种布局——Align
和Flex
。虽然Flutter源码中提供的Box
布局组件远不止这两种,但万变不离其宗,只要深刻理解了BoxConstraints
的本质,相信其他布局也不在话下。
Align布局流程分析
本节将分析Box
布局中比较有代表性的Align
布局,其关键类如图6-3所示。 了解了Align
的布局原理之后,相信对于其他关联的Widget
也能够触类旁通。
图6-3中,RenderShiftedBox
表示一个可以对子节点在自身中的位置进行控制的单子节点容器,最常见的就是Padding
和Align
,其他Widget
可自行研究,每个Widget
都对应一个实现自身布局规则的RenderObject
子类,在此不再赘述。
下面正式分析Align
的布局流程。Align
对应的RenderObject
为RenderPositionedBox
,其performLayout
方法如代码清单6-1所示。
// 代码清单6-1 flutter/packages/flutter/lib/src/rendering/shifted_box.dart
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth =
// 即使约束为infinity也要处理,使之变成有限长度,否则边界无法确定
_widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight
== double.infinity;
if (child != null) {
// 存在子节点
child!.layout(constraints.loosen(), parentUsesSize: true); // 布局子节点
size = constraints.constrain(Size( // 开始布局自身,见代码清单6-2
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.
infinity));
alignChild(); // 计算子节点的偏移,见代码清单6-3
} else {
// 没有子节点时,一般大小为0,因为最大约束为infinity时shrinkWrapWidth为true
size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity));
}
}
以上逻辑中,shrinkWrapWidth
表示当前宽度是否需要折叠(Shrink
),当_widthFactor
被设置或者未对子Widget
做宽度约束时需要,当子Widget
存在时,其大小计算过程如代码清单6-2所示。计算完大小后会调用alignChild
方法完成子Widget
位置的计算。如果子Widget
不存在,则大小默认为0
。
// 代码清单6-2 flutter/packages/flutter/lib/src/rendering/box.dart
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); // 返回约束内最接近自身的值
}
double constrainHeight([ double height = double.infinity ]) {
return height.clamp(minHeight, maxHeight);
}
以上逻辑的核心在于clamp
方法,以constrainWidth
方法为例,其返回值为minWidth
到maxWidth
之间最接近width
的值。以a.clamp(b, c)
为例,将先计算a、b
的较大值x
,再计算x、c
的较小值,并作为最终的结果。下面分析子节点偏移值的计算,如代码清单6-3所示。
// 代码清单6-3 flutter/packages/flutter/lib/src/rendering/shifted_box.dart
void alignChild() {
_resolve(); // 计算子节点的坐标
final BoxParentData childParentData = child!.parentData! as BoxParentData;
// 存储位置信息
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as
Offset); // 偏移值
}
void _resolve() {
if (_resolvedAlignment != null) return;
_resolvedAlignment = alignment.resolve(textDirection);
}
以上逻辑首先会将Alignment
解析成_resolvedAlignment
,其关系如图6-4所示。
图6-4中,RenderPositionedBox
是Align
对应的RenderObject
类型,其通过_resolvedAlignment
字段持有Alignment
的实例,Alignment
就是Align
对子节点位置的抽象表示。Algin
实际持有的是AlignmentGeometry
,它有多个子类,例如AlignmentDirectional
、FractionalOffset
,它们的主要差异在于坐标系的不同,具体可见图6-5和图6-6,在布局阶段,它们将统一转换为Alignment
的布局进行处理。
这里以Alignment
为例进行分析,其逻辑如代码清单6-4所示。
// 代码清单6-4 flutter/packages/flutter/lib/src/painting/alignment.dart
Alignment resolve(TextDirection? direction) => this;
alignChild
方法最终会调用Alignment
的alongOffset
方法完成子节点偏移值的计算,如代码清单6-5所示。
// 代码清单6-5 flutter/packages/flutter/lib/src/painting/alignment.dart
Offset alongOffset(Offset other) {
final double centerX = other.dx / 2.0; // 定位坐标系的原点
final double centerY = other.dy / 2.0; // centerX、centerY为单位距离
return Offset(centerX + x * centerX, centerY + y * centerY); // 根据定位坐标系的坐标计算出在原始坐标系中对应的坐标,并作为偏移值返回
}
以上逻辑中,参数other
表示父节点大小减去子节点后剩余的偏移值,即图6-5中原始坐标系的A
点,其在原始坐标系中的坐标为(other.dx, other.dy)
。由return
语句可知,最终的定位坐标系(图6-5中的虚线坐标系)会在原始坐标系的基础上在X、Y
轴上各移动other
的一半距离。此时,定位坐标系原点在原始坐标系中的坐标为(centerX,centerY)
,即O2
点,原始坐标系的原点O1
在定位坐标系中位置为(–1,–1)
。
由图6-5可知,O1
点(–1,–1)
即父节点的左上角(topLeft
),如代码清单6-6所示。Alignment
的常量其实都是一些特殊坐标。
// 代码清单6-6 flutter/packages/flutter/lib/src/painting/alignment.dart
static const Alignment topLeft = Alignment(-1.0, -1.0); // 见图6-5,O1点,左上角
static const Alignment topCenter = Alignment(0.0, -1.0);
static const Alignment topRight = Alignment(1.0, -1.0); // 见图6-5,B点,右上角
static const Alignment centerLeft = Alignment(-1.0, 0.0);
static const Alignment center = Alignment(0.0, 0.0); // 见图6-5,O2点,中点
static const Alignment centerRight = Alignment(1.0, 0.0);
static const Alignment bottomLeft = Alignment(-1.0, 1.0);
static const Alignment bottomCenter = Alignment(0.0, 1.0);
static const Alignment bottomRight = Alignment(1.0, 1.0); // 见图6-5,A点,右下角
以上是Alignment
的坐标系中常用位置的坐标。FractionalOffset
和AlignmentDirectional
的功能类似,只是坐标系相对父节点的位置不同,如图6-6所示。
事实上,通过坐标系就可以推断出AlignmentGeometry
不同子类的实现细节,在此不再赘述。在实际开发中,应该根据业务场景选择合适的坐标系,而不是一味地借助Alignment
进行Widget
的定位。
Flex布局流程分析
本节分析Flex
布局。Flex
思想在前端领域由来已久,它为有限二维空间内的布局提供了一种灵活且高效的解决方案。Flex
关键类的关系如图6-7所示,Flex
是Flutter中行(Row
)和列(Column
)布局的基础和本质。
图6-7中,Column
和Row
是常见的支持弹性布局的Widget
,它们都继承自Flex
,而Flex
对应的RenderObject
是RenderFlex
。RenderFlex
实现弹性布局的关键,在于其子节点的parentData
字段的类型为FlexParentData
,其内部含有子节点的弹性系数(flex
)等信息。需要注意的是,RenderFlex
控制的是子节点的parentData
字段的类型,而不是自身的字段,因而不是简单的重写(override
)可以解决的,其类定义充分利用了Dart的mixin
特性和泛型语法,远比图6-7所体现的关系要复杂。
由图6-7可知,行、列的布局的底层逻辑都将由RenderFlex
统一完成,因此首先分析RenderFlex
的performLayout
方法,如代码清单6-7所示。
// 代码清单6-7 flutter/packages/flutter/lib/src/rendering/flex.dart
void performLayout() {
final BoxConstraints constraints = this.constraints;
final _LayoutSizes sizes = _computeSizes( // 第1步,对子节点进行布局,见代码清单6-8
layoutChild: ChildLayoutHelper.layoutChild, // 子节点布局函数,即child.layout,
// 见代码清单5-58
constraints: constraints,); // 当前节点(RenderFlex)给子节点的约束条件
final double allocatedSize = sizes.allocatedSize; // 所有子节点占用的空间大小
double actualSize = sizes.mainSize;
double crossSize = sizes.crossSize;
// 第2步,交叉轴大小的校正,见代码清单6-12
// 第3步,计算每个子节点在主轴的偏移值,见代码清单6-13
// 第4步,计算每个子节点在交叉轴的偏移值,见代码清单6-14
}
以上逻辑可分为4步。第1步,执行每个子节点的Layout
流程,计算出子节点所需要占用的空间大小,即主轴方向(即行的水平方向,列的垂直方向)的大小之和。此外,还将计算出交叉轴方向(即行垂直的方向,列的水平方向)的大小,取所有子节点中交叉轴方向最大值。第2步,对于交叉轴方向对齐方式为CrossAxisAlignment.baseline
的情况,重新计算交叉轴方向的大小。这种情况不能简单取交叉轴方向上的最大值,这部分内容后面将详细分析。第3步,根据主轴的对齐方式,确定布局的起始位置和间距。第4步,依次完成每个子节点的布局,即计算每个子节点的偏移值。
首先分析第1步,其逻辑如代码清单6-8所示。
// 代码清单6-8 flutter/packages/flutter/lib/src/rendering/flex.dart
_LayoutSizes _computeSizes( ...... ) {
int totalFlex = 0;
final double maxMainSize = // 计算在当前约束下主轴方向的最大值
_direction == Axis.horizontal ? constraints.maxWidth : constraints.maxHeight;
final bool canFlex = maxMainSize < double.infinity; // 在约束为infinity的情况下,
// 弹性布局没有意义
double crossSize = 0.0;
double allocatedSize = 0.0; // 分配给非弹性节点(non-flexible)的总大小
RenderBox? child = firstChild;
RenderBox? lastFlexChild; // 最后一个Flex类型子节点,使用方式见代码清单6-10
// 计算每个非Flex子节点占用空间的大小和弹性系数之和,见代码清单6-9
// 根据剩余空间,计算每个Flex子节点占用空间的大小,见代码清单6-10
final double idealSize = canFlex && mainAxisSize == MainAxisSize.max
// 最终的mainSize
? maxMainSize : allocatedSize; // 根据MainAxisSize类型计算主轴的实际大小
return _LayoutSizes(mainSize: idealSize, crossSize: crossSize, allocatedSize:
allocatedSize, );
}
以上逻辑中,水平方向(Axis.horizontal
)即Row
的布局,垂直方向即Column
的布局。canFlex
表示是否可以执行弹性布局,仅当主轴大小为有限值时才可以,因为无限大(infinity
)的值除以任意弹性系数,其值仍为无限大,因此此时没有意义。corssSize
表示交叉轴的大小,即Row
的高度和Column
的宽度。
首先计算每个非Flex
子节点占用空间的大小,如代码清单6-9所示。
// 代码清单6-9 flutter/packages/flutter/lib/src/rendering/flex.dart
while (child != null) {
final FlexParentData childParentData = child.parentData! as FlexParentData;
final int flex = _getFlex(child);