背景
最近在做需求的时候,设计页面如上,滑块下面的气泡要按照上面的方式排列,首尾两个滑块在两端,中间的需要在滑块宽度的35%的地方。这时候使用Row组件就不合适了,因为无法精确定位中间的位置。
方案
仔细分析一下,其实我们所需要的就是一个能手动控制子组件位置的widget,但是Flutter中并没有提供类似的(可能有,但是我没找到),不过我们能手动实现一个。
在实现这样的组件之前,我们需要对Flutter的布局流程有个简单的认识:
- 上层组件向下层组件传递约束(constraints)条件。
- 下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
- 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。
而Flutter中布局类组件都直接或间接的继承自SingleChildRenderObjectWidget
和MultiChildRenderObjectWidget
的Widget,比如Row、Column、Stack
等,我们可以参考这些组件来实现一个自定义子组件位置的widget。在自定义组件里我们需要把子组件的位置交由外界来处理。
实现
我们可以自定义一个ManualLayoutWidget
组件,继承自MultiChildRenderObjectWidget
,看过Flutter中一个能获取行数的Wrap这篇文章的可能了解MultiChildRenderObjectWidget
的实现方式,该组件需要一个RenderObject
对象,也就是真正渲染的对象。
在RenderObject对象中,我们重点需要实现三个方法:
setupParentData
: 当子组件被添加到父组件的时候,该方法会设置子组件的parentData
(后续定位会用到)performLayout
:用来布局子组件,并确定自身的size(在该方法里定位每个child的位置)paint
:绘制
可以看到每个child的位置都是在performLayout
方法里决定的,我们需要在该方法里进行:
- 布局子组件
- 将组件自身的size,上一个child的rect,下标以及约束传递给外界,让外界决定child的位置(Offset)
- 根据每个组件的位置、大小决定自身的大小
上面说了大致的思路,下面来看下具体实现:
ini 代码解读复制代码/// 定义方法 外界传回child的位置
/// [childSize] 当前child的大小
/// [previousChildRect] 前一个child的rect
/// [index] 当前child所在的下标
/// [constraints] 组件本身的约束
typedef ManualLayoutChildCallBack = Offset Function(
Size childSize,
Rect? previousChildRect,
int index,
BoxConstraints constraints,
);
@override
void performLayout() {
RenderBox? child = firstChild;
final constraints = this.constraints;
if (child == null) {
size = constraints.smallest;
return;
}
Rect? previousChildRect;
final BoxConstraints childConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
double maxX = .0;
double maxY = .0;
int index = 0;
while (child != null) {
child.layout(childConstraints, parentUsesSize: true);
final childSize = child.size;
final childParentData = child.parentData as _ManualRenderParentData;
final offset = _layoutChild(childSize, previousChildRect, index, constraints);
childParentData.offset = offset;
previousChildRect = offset & childSize;
maxX = math.max(maxX, previousChildRect.right);
maxY = math.max(maxY, previousChildRect.bottom);
child = childParentData.nextSibling;
index = index + 1;
}
// 这里暂时只考虑横向
size = constraints.constrain(Size(constraints.maxWidth, maxY));
}
为什么不用Stack?
为什么不用Stack呢,使用Positioned
也可以自己决定child的位置。不过这种方式有一些缺点,不太符合需求。可以看到滑块和下面的气泡有一个灰色带圆角的背景,这里我是整体用Container包裹的,大致的结构是Container > Column > [滑块 气泡],而Positioned是不参与Stack组件的大小计算的,我们可以看下源码:
ini 代码解读复制代码// 计算Stack的大小
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
... 省去部分代码
double width = constraints.minWidth;
double height = constraints.minHeight;
RenderBox? child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData! as StackParentData;
if (!childParentData.isPositioned) {
hasNonPositionedChildren = true;
final Size childSize = layoutChild(child, nonPositionedConstraints);
width = math.max(width, childSize.width);
height = math.max(height, childSize.height);
}
child = childParentData.nextSibling;
}
final Size size;
if (hasNonPositionedChildren) {
size = Size(width, height);
assert(size.width == constraints.constrainWidth(width));
assert(size.height == constraints.constrainHeight(height));
} else {
size = constraints.biggest;
}
assert(size.isFinite);
return size;
}
这样就无法拿到真实的大小,这样也就无法决定Container的大小了。当然要实现的话也可以,我们可以给Container一个固定的高度,因为在这个例子里气泡的高度其实是固定的。但是尝试另外一种方式不也挺happy嘛?
完整代码可以在github.com/lwy121810/m… 这里查看
评论记录:
回复评论: