Flutter 完整开发实战详解自定义布局

本文详细介绍了如何在Flutter中自定义布局,包括使用MultiChildRenderObjectElement关联自定义RenderBox,自定义ParentData和RenderBox实现,以及利用CustomMultiChildLayout简化过程。重点在于overrideperformLayout方法来控制child的offset实现布局。
摘要由CSDN通过智能技术生成

所以 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 ,并混入 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin 实现 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);

}

如下代码所示,接下来主要看 RenderCloudWidgetoverride 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.parentDataoffset ,来控制其位置。

最后通过 CloudWidget 加载我们的 RenderCloudWidget 即可, 当然完整代码还需要结合 FittedBoxRotatedBox 简化完成,具体可见 :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;

}

}

最后我们总结,实现自定义布局的流程就是,实现自定义 RenderBoxperformLayout child 的 offset

四、CustomMultiChildLayout


CustomMultiChildLayout 是 Flutter 为我们封装的简化自定义布局实现,它的内部同样是通过 MultiChildRenderObjectWidget 实现,但是它为我们封装了 RenderCustomMultiChildLayoutBoxMultiChildLayoutParentData ,并通过 MultiChildLayoutDelegate 暴露出需要自定义的地方。

使用 CustomMultiChildLayout 你只需要继承 MultiChildLayoutDelegate ,并实现如下方法即可:

void performLayout(Size size);

bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);

通过继承 MultiChildLayoutDelegate,并且实现 performLayout 方法,我们可以快速自定义我们需要的控件,当然便捷的封装也代表了灵活性的丧失,可以看到 performLayout 方法中只有布局自身的 Size 参数,所以完成上图需求时,我们还需要 child 的大小和位置 ,也就是 childSizechildId

childSize 相信大家都能故名思义,那 childId 是什么呢?

这就要从 MultiChildLayoutDelegate 的实现说起,MultiChildLayoutDelegate 内部会有一个 Map<Object, RenderBox> _idToChild; 对象,这个 Map 对象保存着 Object idRenderBox 的映射关系,而在 MultiChildLayoutDelegate 中获取 RenderBox 都需要通过 id 获取。

_idToChild 这个 Map 是在 RenderBox performLayout 时,在 delegate._callPerformLayout 方法内创建的,创建后所用的 idMultiChildLayoutParentData 中的 id, MultiChildLayoutParentData 的 id ,可以通过 LayoutId 嵌套时自定义指定赋值。

而完成上述布局,我们需要知道每个 child 的 index ,所以我们可以把 index 作为 id 设置给每个 child 的 LayoutId

所以我们可以通过 LayoutId 指定 id 为数字 index , 同时告知 delegate ,这样我们就知道 child 顺序和位置啦。

这个 id 是 Object 类型 ,所以你懂得,你可以赋予很多属性进去。

如下代码所示,这样在自定义的 CircleLayoutDelegate 中,就知道每个控件的 index 位置,也就是知道了,圆形布局中每个 item 需要的位置。

我们只需要通过 index ,计算出 child 所在的角度,然后利用 layoutChildpositionChild 对每个item进行布局即可,完整代码:GSYFlutterDemo

///自定义实现圆形布局

class CircleLayoutDelegate extends MultiChildLayoutDelegate {

final List customLayoutId;

final Offset center;

Size childSize;

CircleLayoutDelegate(this.customLayoutId,

{this.center = Offset.zero, this.childSize});

@override

void performLayout(Size size) {

for (var item in customLayoutId) {

if (hasChild(item)) {

double r = 100;

int index = int.parse(item);

double step = 360 / customLayoutId.length;

double hd = (2 * math.pi / 360) * step * index;

var x = center.dx + math.sin(hd) * r;

var y = center.dy - math.cos(hd) * r;

childSize ??= Size(size.width / customLayoutId.length,

先自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以扫码领取!!!!

最后

感谢您的阅读,在文末给大家准备一个福利。本人从事Android开发已经有十余年,算是一名资深的移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

移动开发架构师了吧。根据我的观察发现,对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

所以在此将我十年载,从萌新小白一步步成长为Android移动开发架构师的学习笔记,从Android四大组件到手写实现一个架构设计,我都有一一的对应笔记为你讲解。

当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。

[外链图片转存中…(img-AulmoxTg-1711246800825)]

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值