其实对于了解flutter的人来说,你可能已经知道flutter自身也有流式布局控件,那就是Wrap和Flow,Wrap易用而Flow更灵活,关于这两个组件的用法在这里不做介绍,可自行搜索。那为什么还要自定义流式布局控件呢?现实开发中,往往有这样的需求,流式布局中的label数量是动态的,而又不能显示过多,导致整个页面都是流控件,这样也不美观,这个时候需求就来了:最大显示3行。虽然看似是一个简单的属性,但这个时候wrap和flow就很难实现了。
进入正题,首先流式控件是一个有多个子控件的控件,它的夫容器必需可以容纳多个子控件。有点类似于Android中的ViewGroup。另外,Wrap和Flow本身不就是这样的控件吗?拿来改造不更方便吗。经过调研,发现Flutter控件源码是可以拿来直接用的,不像是Android源码使用了很多隐藏api,直接拿出来编译都不过。首先我们定义MyFlow继承MultiChildRenderObjectWidget,以获得拥有多个child控件的能力。另外,它还要有一些常用属性,padding(边距),spacing(child之间的横向间隔),runSpacing(child之间的纵向间隔),maxLine(我们想要属性,最大行数)。如下:
class MyFlow extends MultiChildRenderObjectWidget {
final EdgeInsets padding;
final double spacing;
final double runSpacing;
final int maxLine;
MyFlow({Key key,
this.padding =const EdgeInsets.all(0),
this.spacing =10,
this.runSpacing =10,
this.maxLine =3,
List children =const []})
:assert(padding !=null),super(key: key, children: RepaintBoundary.wrapAll(children));
@override
RenderObject createRenderObject(BuildContext context) {
return MyRenderFlow(
padding:padding,
spacing:spacing,
runSpacing:runSpacing,
maxLine:maxLine);
}
@override
void updateRenderObject(BuildContext context, IKRenderFlow renderObject) {
renderObject
..padding =padding
..spacing =spacing
..runSpacing =runSpacing
..maxLine =maxLine;
}
}
通过研究,我们发现继承关系MultiChildRenderObjectWidget->RenderObjectWidget->Widget,
接下来实现renderObjectWidget内的这个方法,用于创建要渲染的对象:
RenderObject createRenderObject(BuildContext context);
接下来就要实现我们自己的RenderObject类了,流式布局主要在这里实现,上面是对外公开的组件。我们要实现的RenderObject中,要实现两个功能:
1.对children测量和实行流式布局和绘制
2.对自己大小动态计算
通过对Flow的学习我们需要继承ContainerRenderObjectMixin(提供了对children的管理功能)RenderBoxContainerDefaultsMixin(提供了对children的绘制、点击响应等功能)。
///每个child都带一个parentData,在这里可以定义想用的属性
class _MyFlowParentData extends ContainerBoxParentData<RenderBox> {
//是否可用
bool _dirty = false;
}
///主要实现
class MyRenderFlow extends RenderBox with
ContainerRenderObjectMixin<RenderBox, _MyFlowParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _MyFlowParentData> {
EdgeInsets _padding;
set padding(EdgeInsets padding) {
if (padding == null) {
return;
}
this._padding = padding;
}
double _spacing;
set spacing(double spacing) {
if (spacing == null) {
return;
}
this._spacing = spacing;
}
double _runSpacing;
set runSpacing(double runSpacing) {
if (runSpacing == null) {
return;
}
this._runSpacing = runSpacing;
}
int _maxLine;
set maxLine(int maxLine) {
if (maxLine == null) {
return;
}
this._maxLine = maxLine;
}
MyRenderFlow(
{EdgeInsets padding = const EdgeInsets.all(0),
double spacing = 10,
double runSpacing = 10,
int maxLine = 3})
: assert(padding != null),
_padding = padding,
_spacing = spacing,
_runSpacing = runSpacing,
_maxLine = maxLine;
@override
bool get isRepaintBoundary => true;
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _MyFlowParentData)
child.parentData = _MyFlowParentData();
}
//核心方法,计算每个child的offset,也就是想对于原点的偏移位置,最终算出来满足条件的要参与layout和paint的children,
//然后根据要显示的children的高度,算出窗口高度。
//不参与显示的child打上_dirty=ture的标记。
double _computeIntrinsicHeightForWidth(double width) {
int runCount = 0;
double height = _padding.top;
double runWidth = _padding.left;
double runHeight = 0.0;
int childCount = 0;
RenderBox child = firstChild;
while (child != null) {
final double childWidth = child.getMaxIntrinsicWidth(double.infinity);
final double childHeight = child.getMaxIntrinsicHeight(childWidth);
final _MyFlowParentData childParentData = child.parentData;
if (runWidth + childWidth + _padding.right > width) {
if (_maxLine > 0 && runCount + 1 == _maxLine) {
childParentData._dirty = true;
child = childAfter(child);
continue;
}
childParentData._dirty = false;
height += runHeight;
if (runCount > 0) {
height += _runSpacing;
}
runCount += 1;
runWidth = _padding.left;
runHeight = 0.0;
childCount = 0;
}
//更新绘制位置start
childParentData.offset = Offset(
runWidth + ((childCount > 0) ? _spacing : 0),
height + ((runCount > 0) ? _runSpacing : 0));
//更新绘制位置end
runWidth += childWidth;
runHeight = math.max(runHeight, childHeight);
if (childCount > 0) {
runWidth += _spacing;
}
childCount += 1;
child = childAfter(child);
}
if (childCount > 0) {
height += runHeight + _runSpacing + _padding.bottom;
}
return height;
}
//因为是纵向换行,横向固定使用父控限定的最大宽度
double _computeIntrinsicWidthForHeight(double height) {
return constraints.maxWidth;
}
@override
double computeMinIntrinsicWidth(double height) {
double width = _computeIntrinsicWidthForHeight(height);
return width;
}
@override
double computeMaxIntrinsicWidth(double height) {
double width = _computeIntrinsicWidthForHeight(height);
return width;
}
@override
double computeMinIntrinsicHeight(double width) {
double height = _computeIntrinsicHeightForWidth(width);
return height;
}
@override
double computeMaxIntrinsicHeight(double width) {
double height = _computeIntrinsicHeightForWidth(width);
return height;
}
@override
void performLayout() {
RenderBox child = firstChild;
if (child == null) {
size = constraints.smallest;
return;
}
size = Size(_computeIntrinsicWidthForHeight(constraints.maxHeight),
_computeIntrinsicHeightForWidth(constraints.maxWidth));
//布局每个child,_dirty的child自动忽略
while (child != null) {
final BoxConstraints innerConstraints = constraints.loosen();
final _MyFlowParentData childParentData = child.parentData;
if (!childParentData._dirty) {
child.layout(innerConstraints, parentUsesSize: true);
}
child = childParentData.nextSibling;
}
}
@override
void paint(PaintingContext context, Offset offset) {
RenderBox child = firstChild;
//绘制每个child
while (child != null) {
final _MyFlowParentData childParentData = child.parentData;
if (!childParentData._dirty) {
context.paintChild(child, childParentData.offset + offset);
}
child = childParentData.nextSibling;
}
}
@override
bool hitTestChildren(HitTestResult result, {Offset position}) {
//响应点击区域,因为布局和绘制是同样的位置 ,没有偏移,所以使用默认逻辑
return defaultHitTestChildren(result, position: position);
}
}
好,到这里就实现完了,通过这种思路,我们不仅可以实现流式布局,其它的行为也是一样的。对于刚接手不清楚每个控件的含义的同学,这里的技巧就是找一个行为相进的控件去模仿、改造,这样能大大加快学习的脚步。
付上效果图: