我们经常有对齐的诉求,比如一个组件在一个大组件的左上角,右下角,中间显示等等,Container 中可以设置 alignment
属性来实现。Flutter 也有专门的布局组件来实现同样的效果,那就是 Align。
这一篇文章,我就全方位介绍一下 Align 组件,从南到北(基本使用,原理)。
Align 组件介绍
Align 组件的功能主要有两个:对齐其内部的子节点,基于子节点的大小调整自己的大小。
效果是这样的:
我们举个例子,你想要让子节点显示在右下角,那么可以设置属性为 Alignment.bottomRight
,但是有个前提需要 Align 本身的尺寸大于子节点。
对于对齐来说,最起码外圈尺寸是要大于子节点的大小,两个要是一样大,肯定就重叠了呀。我们看外圈也就是 Align 的尺寸,默认是多少呢?会尽可能大的,父节点给 Align 的约束是多少,那么 Align 就取约束的最大值。
当然了也有非默认的规则:
如果没有约束并且
widthFactor
和heightFactor
也没设置,那么 Align 的大小就是子节点的大小。如果
widthFactor
和heightFactor
设置了,那么 Align 的大小就是子节点的大小与因子的运算值。比如 widthFactor 是 2.0,那么 Align 的宽度就是子节点宽度的两倍。
现在我们知道了 Alin 是啥,下面我们看 Align 的属性。
Align 的属性
属性 | 类型 | 作用 |
---|---|---|
key |
Key? | 组件的标示 |
alignment |
AlignmentGeometry | 子节点的对齐方法,一般设置为 Alignment
|
widthFactor |
double? | 宽度因子,Align 的宽度 = 子节点的宽度 * 宽度因子 |
heightFactor |
double? | 高度因子,Align 的高度 = 子节点的高度 * 高度因子 |
child |
Widget? | 待对齐的子节点 |
alignment 对齐
这个属性用来控制对齐,虽然类型是 AlignmentGeometry
但是我们常用 Alignment 来赋值。
Alignment 是用构造方法的 x 和 y 来控制位置,范围是 -1 到 1 。如果 x 的值是 -1 ,子节点放在 Align 的左边,1 表示子节点会放在 Align 的右边,同理 y 的 -1 表示子节点在 Align 的上边,1 表示在下边。我们在下面会介绍具体的计算过程。
widthFactor
宽度因子
如果设置非 null,那么 Align 的宽度就是 子节点的宽度与因子的乘积,这个值不能是负数。
heightFactor
高度因子
如果设置非 null,那么 Align 的高度就是 子节点的高度与因子的乘积,这个值不能是负数。
知道了这些属性,下面我们使用效果。
Align 使用
Align 的外圈是黑色边框的 Container,子节点是 60*60 的蓝色色块
基本使用
基础代码如下:
Container(
width: 300,
height: 300,
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: Align(
child: Container(
height: 60,
width: 60,
color: Colors.blue,
),
),
)
Align 的布局默认居中,可以调整其 aligment
属性
Align 摆放原理
老规矩 三棵树最终章, Align
是渲染型组件,它的渲染对象是 RenderPositionedBox
,所以我们去 RenderPositionedBox
看摆放的原理。
ParentData 父节点需要知道的数据
在介绍摆放之前,我们先介绍一个概念 ParentData
,就是父渲染对象想要知道的数据。
比如字节点的位置等等, 对应到代码中就是 RenderObject 的 parentData
属性。
RenderObject 的 ParentData
分为两大类:盒子数据 和 Sliver 数据,我们以 ContainerParentDataMixin
为例,看看存储的内容:
mixin ContainerParentDataMixin<ChildType extends RenderObject> on ParentData {
ChildType? previousSibling;
ChildType? nextSibling;
@override
void detach() {
super.detach();
}
}
从这个数据 model 中,父节点可以知道某个子节点的前后节点是谁。 ChildType 是继承自 RenderObject
的范型。
我们熟知的 Row/Column、Stack、Wrap 等组件对应的渲染对象持有的 parentData 都是 ContainerParentDataMixin 的子类型。所以它们的渲染对象在布局的时候,才可以依次布局子节点。
RenderPositionedBox
的 parentData
是 BoxParentData
。BoxParentData 的定义如
下:
class BoxParentData extends ParentData {
/// The offset at which to paint the child in the parent's coordinate system.
Offset offset = Offset.zero;
@override
String toString() => 'offset=$offset';
}
offset
就是 Align
字节点在 Align
中的位置,看到这里你就明白了,对齐就是这个值的计算。下面我们看计算的过程。
布局过程
摆放就是布局测量和绘制。分别对应着 performLayout
和 paint
布局过程如下:
- 第一:确定布局子节点,确定子节点的大小
- 第二:根据缩放因子和子节点的大小确定自己的大小
- 第三:根据对齐属性确定子节点的位置
- 第四:根据位置进行绘制
下面我们从代码中来看具体的过程:
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth = _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(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
)); //第二处
alignChild(); //第三处
} else {
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}
我们看第一处的代码,第一处是布局子节点,这个入参非常有意思:constraints.loosen()
和 parentUsesSize
。
constraints.loosen()
的作用是子节点的约束是宽松的:0 – Align的最大宽度,所以 Align 的子节点最大可用空间不会超过 Align,超过会怎么办呢?截断! 不会出现 over 那样的溢出提示。
parentUsesSize
的作用是告诉 framework,Align 的尺寸信息依赖我的子节点,如果我的子节点标记为 dirty 了,请带上我。
所以第一处的代码是确定子节点的尺寸,我们看第二处的代码,第二处是根据子节点的大小来确定自己的大小。我们以宽度为例:
标题 | 宽度因子不是null | 宽度因子是 null |
---|---|---|
约束无限 |
true 子节点宽度 * 宽度因子 |
true 子节点宽度 |
约束有限 |
true 子节点宽度 * 宽度因子 |
false 宽度无限(最大宽度) |
挺有意思吧~,只要设置了尺寸因子,那么 Align 的尺寸就是子节点的大小与尺寸因子的乘积了。
只要不设置,要么就是子节点的宽度,要么就是父布局的宽度,这样就实现了子节点在其爷爷节点中的对齐,Align 只是桥梁而已。
我们第三处,对齐子节点。
@protected
void alignChild() {
_resolve();
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}
Offset alongOffset(Offset other) {
final double centerX = other.dx / 2.0;
final double centerY = other.dy / 2.0;
return Offset(centerX + x * centerX, centerY + y * centerY);
}
对齐的就是确定了我们上面讲到的确定 offset
,确定的方式就是 alongOffset
。
我们可以暂时先想一下怎么确定位置?
Flutter 的处理是先居中对齐,然后左减右加,加减的范围就是 Alignment 构造方法中 x,y 的绝对值。
比如 Align 的宽度范围是 0 —— 120, child 的宽度是 60。
所以居中的位置是 (120 – 60 )/ 2 = 30, child 的范围就是 30 – 90。
我们在看左上角 Alignment topLeft = Alignment(-1.0, -1.0)
的计算过程。
首先,先确定居中的位置 30
其次,确定宽度 centerX + x * centerX ,就是 30 + (-1)* 30 = 0
最后,x 的坐标就是 0
所以,才有文章开头动画中范围是 -1 到 1,-1 代表最左边和最上边,1 代表最右边和最下边,其他的数值,在这个范围浮动。
这就是位置 offset
的计算过程,有了这个 offset
,有什么用呢?
按着这个位置绘制和响应手势!!
绘制过程
RenderPositionedBox
并没有绘制的 paint 方法,绘制方法在其父类 RenderShiftedBox
中。
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
context.paintChild(child!, childParentData.offset + offset);
}
}
我们看到就是在 Align 的基础上增加布过程中计算好的的 offset
。
比如上面布局过程计算好的 offset 是 (20,20),那么它的真实位置就是 Align 左上角的坐标,向右向下偏移(20,20),偏移后的就是坐标就是 Align 子节点真实的绘制坐标。
手势响应范围
我们知道手势是有测试范围的,一般是组件的范围内,也会增加是否落在了自己的组件上。如下:
bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
上面是通用的处理方式,如果落点在自己的组件范围内,会继续判断是否落在了子节点上 hitTestChildren
,是否自己有额外的判断 hitTestSelf
。
我们看 Align
渲染对象的 hitTestChildren
。
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,// 第一处
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed);
},
);
}
return false;
}
我们看到在检测自己的子节点是否满足点击的时候,加了一个偏移转换,转换的坐标就是布局阶段的 offset 。
通过上面的绘制和点击检测,我们可以清晰的理解 ParentData 的作用,它携带的数据就是给 Align 组件用的。
总结
这一篇就结束啦,这是布局组件的第一篇。介绍了 Align 的使用场景、基本属性、基本使用。在这些的基础上,探究了 Align 能够实现对齐的原理,浓缩成一句话就是:先找到中间,在计算因子。我们使用的 Center 组件就是 Align 的子类,只是将对齐属性设置为了居中而已。