Flutter 完整开发实战详解自定义布局,移动开发框架2019
/// 计算返回第一个 child 的基线 ,常用于 child 的位置顺序有关
double defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline)
/// 计算返回所有 child 中最小的基线,常用于 child 的位置顺序无关
double defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline)
/// 触摸碰撞测试
bool defaultHitTestChildren(BoxHitTestResult result, { Offset position })
/// 默认绘制
void defaultPaint(PaintingContext context, Offset offset)
/// 以数组方式返回 child 链表
List getChildrenAsList()
3、ContainerBoxParentData
ContainerBoxParentData 是 BoxParentData 的子类,主要是关联了 ContainerDefaultsMixin 和 BoxParentData ,BoxParentData 是 RenderBox 绘制时所需的位置类。
通过 ContainerBoxParentData ,我们可以将 RenderBox 需要的 BoxParentData 和上面的 ContainerParentDataMixin 组合起来,事实上我们得到的 children 双链表就是以 ParentData 的形式呈现出来的。
abstract class ContainerBoxParentData extends BoxParentData with ContainerParentDataMixin { }
4、MultiChildRenderObjectWidget
MultiChildRenderObjectWidget 的实现很简单 ,它仅仅只是继承了 RenderObjectWidget,然后提供了 children 数组,并创建了 MultiChildRenderObjectElement。
上面的 RenderObjectWidget 顾名思义,它是提供 RenderObject 的 Widget ,那有不存在 RenderObject 的 Widget 吗?
有的,比如我们常见的 StatefulWidget 、 StatelessWidget 、 Container 等,它们的 Element 都是 ComponentElement , ComponentElement 仅仅起到容器的作用,而它的 get renderObject 需要来自它的 child 。
5、MultiChildRenderObjectElement
前面的篇章我们说过 Element 是 BuildContext 的实现, 内部一般持有 Widget 、RenderObject 并作为二者沟通的桥梁,那么 MultiChildRenderObjectElement 就是我们自定义布局时的桥梁了, 如下代码所示,MultiChildRenderObjectElement 主要实现了如下接口,其主要功能是对内部 children 的 RenderObject ,实现了插入、移除、访问、更新等逻辑:
/// 下面三个方法都是利用 ContainerRenderObjectMixin 的 in
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
sert/move/remove 去操作
/// ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin
void insertChildRenderObject(RenderObject child, Element slot)
void moveChildRenderObject(RenderObject child, dynamic slot)
void removeChildRenderObject(RenderObject child)
/// visitChildren 是通过 Element 中的 ElementVisitor 去迭代的
/// 一般在 RenderObject get renderObject 会调用
void visitChildren(ElementVisitor visitor)
/// 添加忽略child _forgottenChildren.add(child);
void forgetChild(Element child)
/// 通过 inflateWidget , 把 children 中 List 对应的 List
void mount(Element parent, dynamic newSlot)
/// 通过 updateChildren 方法去更新得到 List
void update(MultiChildRenderObjectWidget newWidget)
所以 MultiChildRenderObjectElement 利用 ContainerRenderObjectMixin 最终将我们自定义的 RenderBox 和 Widget 关联起来。
6、自定义流程
上述主要描述了 MultiChildRenderObjectWidget 、 MultiChildRenderObjectElement 和其他三个辅助类ContainerRenderObjectMixin 、 RenderBoxContainerDefaultsMixin 和 ContainerBoxParentData 之间的关系。
了解几个关键类之后,我们看一般情况下,实现自定义布局的简化流程是:
1、自定义 ParentData 继承 ContainerBoxParentData 。
2、继承 RenderBox ,同时混入 ContainerRenderObjectMixin 和 RenderBoxContainerDefaultsMixin 实现自定义RenderObject 。
3、继承 MultiChildRenderObjectWidget,实现 createRenderObject 和 updateRenderObject 方法,关联我们自定义的 RenderBox。
4、override RenderBox 的 performLayout 和 setupParentData 方法,实现自定义布局。
当然我们可以利用官方的 CustomMultiChildLayout 实现自定义布局,这个后面也会讲到,现在让我们先从基础开始, 而上述流程中混入的 ContainerRenderObjectMixin 和 RenderBoxContainerDefaultsMixin ,在 RenderFlex 、RenderWrap 、RenderStack 等官方实现的布局里,也都会混入它们。
自定义布局就是在 performLayout 中实现的 child.layout 大小和 child.ParentData.offset 位置的赋值。
首先我们要实现类似如图效果,我们需要自定义 RenderCloudParentData
继承 ContainerBoxParentData
,用于记录宽高和内容区域 :
class RenderCloudParentData extends ContainerBoxParentData {
double width;
double height;
Rect get content => Rect.fromLTWH(
offset.dx,
offset.dy,
width,
height,
);
}
然后自定义 RenderCloudWidget
继承 RenderBox
,并混入 ContainerRenderObjectMixin
和 RenderBoxContainerDefaultsMixin
实现 RenderBox
自定义的简化。
class RenderCloudWidget extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, RenderCloudParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, RenderCloudParentData> {
RenderCloudWidget({
List children,
Overflow overflow = Overflow.visible,
double ratio,
}) : _ratio = ratio,
_overflow = overflow {
///添加所有 child
addAll(children);
}
如下代码所示,接下来主要看 RenderCloudWidget
中override performLayout
中的实现,这里我们只放关键代码:
-
1、我们首先拿到
ContainerRenderObjectMixin
链表中的firstChild
,然后从头到位读取整个链表。 -
2、对于每个 child 首先通过
child.layout
设置他们的大小,然后记录下大小之后。 -
3、以容器控件的中心为起点,从内到外设置布局,这是设置的时候,需要通过记录的
Rect
判断是否会重复,每次布局都需要计算位置,直到当前 child 不在重复区域内。 -
4、得到最终布局内大小,然后设置整体居中。
///设置为我们的数据
@override
void setupParentData(RenderBox child) {
if (child.parentData is! RenderCloudParentData)
child.parentData = RenderCloudParentData();
}
@override
void performLayout() {
///默认不需要裁剪
_needClip = false;
///没有 childCount 不玩
if (childCount == 0) {
size = constraints.smallest;
return;
}
///初始化区域
var recordRect = Rect.zero;
var previousChildRect = Rect.zero;
RenderBox child = firstChild;
while (child != null) {
var curIndex = -1;
///提出数据
final RenderCloudParentData childParentData = child.parentData;
child.layout(constraints, parentUsesSize: true);
var childSize = child.size;
///记录大小
childParentData.width = childSize.width;
childParentData.height = childSize.height;
do {
///设置 xy 轴的比例
var rX = ratio >= 1 ? ratio : 1.0;
var rY = ratio <= 1 ? ratio : 1.0;
///调整位置
var step = 0.02 * _mathPi;
var rotation = 0.0;
var angle = curIndex * step;
var angleRadius = 5 + 5 * angle;
var x = rX * angleRadius * math.cos(angle + rotation);
var y = rY * angleRadius * math.sin(angle + rotation);
var position = Offset(x, y);
///计算得到绝对偏移
var childOffset = position - Alignment.center.alongSize(childSize);
++curIndex;
///设置为遏制
childParentData.offset = childOffset;
///判处是否交叠
} while (overlaps(childParentData));
///记录区域
previousChildRect = childParentData.content;
recordRect = recordRect.expandToInclude(previousChildRect);
///下一个
child = childParentData.nextSibling;
}
///调整布局大小
size = constraints
.tighten(
height: recordRect.height,
width: recordRect.width,
)
.smallest;
///居中
var contentCenter = size.center(Offset.zero);
var recordRectCenter = recordRect.center;
var transCenter = contentCenter - recordRectCenter;
child = firstChild;
while (child != null) {
final RenderCloudParentData childParentData = child.parentData;
childParentData.offset += transCenter;
child = childParentData.nextSibling;
}
///超过了嘛?
_needClip =
size.width < recordRect.width || size.height < recordRect.height;
}
其实看完代码可以发现,关键就在于你怎么设置 child.parentData
的 offset
,来控制其位置。
最后通过 CloudWidget
加载我们的 RenderCloudWidget
即可, 当然完整代码还需要结合 FittedBox
与 RotatedBox
简化完成,具体可见 :GSYFlutterDemo
class CloudWidget extends MultiChildRenderObjectWidget {
final Overflow overflow;
final double ratio;
CloudWidget({
Key key,
this.ratio = 1,
this.overflow = Overflow.clip,
List children = const [],
}) : super(key: key, children: children);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCloudWidget(
ratio: ratio,
overflow: overflow,
);
}
@override
void updateRenderObject(
BuildContext context, RenderCloudWidget renderObject) {
renderObject
…ratio = ratio
…overflow = overflow;
}
}
最后我们总结,实现自定义布局的流程就是,实现自定义 RenderBox
中 performLayout
child 的 offset
。
CustomMultiChildLayout
是 Flutter 为我们封装的简化自定义布局实现,它的内部同样是通过 MultiChildRenderObjectWidget
实现,但是它为我们封装了 RenderCustomMultiChildLayoutBox
和 MultiChildLayoutParentData
,并通过 MultiChildLayoutDelegate
暴露出需要自定义的地方。