其實(shí)對(duì)于了解flutter的人來(lái)說(shuō),你可能已經(jīng)知道flutter自身也有流式布局控件,那就是Wrap和Flow,Wrap易用而Flow更靈活,關(guān)于這兩個(gè)組件的用法在這里不做介紹,可自行搜索。那為什么還要自定義流式布局控件呢?現(xiàn)實(shí)開(kāi)發(fā)中,往往有這樣的需求,流式布局中的label數(shù)量是動(dòng)態(tài)的,而又不能顯示過(guò)多,導(dǎo)致整個(gè)頁(yè)面都是流控件,這樣也不美觀,這個(gè)時(shí)候需求就來(lái)了:最大顯示3行。雖然看似是一個(gè)簡(jiǎn)單的屬性,但這個(gè)時(shí)候wrap和flow就很難實(shí)現(xiàn)了。
進(jìn)入正題,首先流式控件是一個(gè)有多個(gè)子控件的控件,它的夫容器必需可以容納多個(gè)子控件。有點(diǎn)類似于Android中的ViewGroup。另外,Wrap和Flow本身不就是這樣的控件嗎?拿來(lái)改造不更方便嗎。經(jīng)過(guò)調(diào)研,發(fā)現(xiàn)Flutter控件源碼是可以拿來(lái)直接用的,不像是Android源碼使用了很多隱藏api,直接拿出來(lái)編譯都不過(guò)。首先我們定義MyFlow繼承MultiChildRenderObjectWidget,以獲得擁有多個(gè)child控件的能力。另外,它還要有一些常用屬性,padding(邊距),spacing(child之間的橫向間隔),runSpacing(child之間的縱向間隔),maxLine(我們想要屬性,最大行數(shù))。如下:
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;
}
}
通過(guò)研究,我們發(fā)現(xiàn)繼承關(guān)系MultiChildRenderObjectWidget->RenderObjectWidget->Widget,
接下來(lái)實(shí)現(xiàn)renderObjectWidget內(nèi)的這個(gè)方法,用于創(chuàng)建要渲染的對(duì)象:
RenderObject createRenderObject(BuildContext context);
接下來(lái)就要實(shí)現(xiàn)我們自己的RenderObject類了,流式布局主要在這里實(shí)現(xiàn),上面是對(duì)外公開(kāi)的組件。我們要實(shí)現(xiàn)的RenderObject中,要實(shí)現(xiàn)兩個(gè)功能:
1.對(duì)children測(cè)量和實(shí)行流式布局和繪制
2.對(duì)自己大小動(dòng)態(tài)計(jì)算
通過(guò)對(duì)Flow的學(xué)習(xí)我們需要繼承ContainerRenderObjectMixin(提供了對(duì)children的管理功能)RenderBoxContainerDefaultsMixin(提供了對(duì)children的繪制、點(diǎn)擊響應(yīng)等功能)。
///每個(gè)child都帶一個(gè)parentData,在這里可以定義想用的屬性
class _MyFlowParentData extends ContainerBoxParentData<RenderBox> {
//是否可用
bool _dirty = false;
}
///主要實(shí)現(xiàn)
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();
}
//核心方法,計(jì)算每個(gè)child的offset,也就是想對(duì)于原點(diǎn)的偏移位置,最終算出來(lái)滿足條件的要參與layout和paint的children,
//然后根據(jù)要顯示的children的高度,算出窗口高度。
//不參與顯示的child打上_dirty=ture的標(biāo)記。
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;
}
//因?yàn)槭强v向換行,橫向固定使用父控限定的最大寬度
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));
//布局每個(gè)child,_dirty的child自動(dòng)忽略
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;
//繪制每個(gè)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}) {
//響應(yīng)點(diǎn)擊區(qū)域,因?yàn)椴季趾屠L制是同樣的位置 ,沒(méi)有偏移,所以使用默認(rèn)邏輯
return defaultHitTestChildren(result, position: position);
}
}
好,到這里就實(shí)現(xiàn)完了,通過(guò)這種思路,我們不僅可以實(shí)現(xiàn)流式布局,其它的行為也是一樣的。對(duì)于剛接手不清楚每個(gè)控件的含義的同學(xué),這里的技巧就是找一個(gè)行為相進(jìn)的控件去模仿、改造,這樣能大大加快學(xué)習(xí)的腳步。
付上效果圖:
