• Flutter 布局(九)- Flow、Table、Wrap详解
    • 1. Flow
      • 1.1 简介
      • 1.2 布局行为
      • 1.3 继承关系
      • 1.4 示例代码
      • 1.5 源码解析
        • 1.5.1 属性解析
        • 1.5.2 源码
      • 1.6 使用场景
    • 2. Table
      • 2.1 简介
      • 2.2 布局行为
      • 2.3 继承关系
      • 2.4 示例代码
      • 2.5 源码解析
        • 2.5.1 属性解析
        • 2.5.2 源码
      • 2.6 使用场景
    • 3. Wrap
      • 3.1 简介
      • 3.2 布局行为
      • 3.3 继承关系
      • 3.4 示例代码
      • 3.5 源码解析
        • 3.5.1 属性解析
        • 3.5.2 源码
      • 3.6 使用场景
    • 4. 后话
    • 5. 参考

    Flutter 布局(九)- Flow、Table、Wrap详解

    本文主要介绍Flutter布局中的Flow、Table、Wrap控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析。

    1. Flow

    A widget that implements the flow layout algorithm.

    1.1 简介

    Flow按照解释的那样,是一个实现流式布局算法的控件。流式布局在大前端是很常见的布局方式,但是一般使用Flow很少,因为其过于复杂,很多场景下都会去使用Wrap。

    1.2 布局行为

    Flow官方介绍是一个对child尺寸以及位置调整非常高效的控件,主要是得益于其FlowDelegate。另外Flow在用转换矩阵(transformation matrices)对child进行位置调整的时候进行了优化。

    Flow以及其child的一些约束都会受到FlowDelegate的控制,例如重写FlowDelegate中的geiSize,可以设置Flow的尺寸,重写其getConstraintsForChild方法,可以设置每个child的布局约束条件。

    Flow之所以高效,是因为其在定位过后,如果使用FlowDelegate中的paintChildren改变child的尺寸或者位置,只是重绘,并没有实际调整其位置。

    1.3 继承关系

    1. Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flow

    1.4 示例代码

    1. const width = 80.0;
    2. const height = 60.0;
    3. Flow(
    4. delegate: TestFlowDelegate(margin: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0)),
    5. children: <Widget>[
    6. new Container(width: width, height: height, color: Colors.yellow,),
    7. new Container(width: width, height: height, color: Colors.green,),
    8. new Container(width: width, height: height, color: Colors.red,),
    9. new Container(width: width, height: height, color: Colors.black,),
    10. new Container(width: width, height: height, color: Colors.blue,),
    11. new Container(width: width, height: height, color: Colors.lightGreenAccent,),
    12. ],
    13. )
    14. class TestFlowDelegate extends FlowDelegate {
    15. EdgeInsets margin = EdgeInsets.zero;
    16. TestFlowDelegate({this.margin});
    17. @override
    18. void paintChildren(FlowPaintingContext context) {
    19. var x = margin.left;
    20. var y = margin.top;
    21. for (int i = 0; i < context.childCount; i++) {
    22. var w = context.getChildSize(i).width + x + margin.right;
    23. if (w < context.size.width) {
    24. context.paintChild(i,
    25. transform: new Matrix4.translationValues(
    26. x, y, 0.0));
    27. x = w + margin.left;
    28. } else {
    29. x = margin.left;
    30. y += context.getChildSize(i).height + margin.top + margin.bottom;
    31. context.paintChild(i,
    32. transform: new Matrix4.translationValues(
    33. x, y, 0.0));
    34. x += context.getChildSize(i).width + margin.left + margin.right;
    35. }
    36. }
    37. }
    38. @override
    39. bool shouldRepaint(FlowDelegate oldDelegate) {
    40. return oldDelegate != this;
    41. }
    42. }

    样例其实并不复杂,FlowDelegate需要自己实现child的绘制,其实大多数时候就是位置的摆放。上面例子中,对每个child按照给定的margin值,进行排列,如果超出一行,则在下一行进行布局。

    Flow样例

    另外,对这个例子多做一个说明,对于上述child宽度的变化,这个例子是没问题的,如果每个child的高度不同,则需要对代码进行调整,具体的调整是换行的时候,需要根据上一行的最大高度来确定下一行的起始y坐标。

    1.5 源码解析

    构造函数如下:

    1. Flow({
    2. Key key,
    3. @required this.delegate,
    4. List<Widget> children = const <Widget>[],
    5. })

    1.5.1 属性解析

    delegate:影响Flow具体布局的FlowDelegate。

    其中FlowDelegate包含如下几个方法:

    • getConstraintsForChild: 设置每个child的布局约束条件,会覆盖已有的;
    • getSize:设置Flow的尺寸;
    • paintChildren:child的绘制控制代码,可以调整尺寸位置,写起来比较的繁琐;
    • shouldRepaint:是否需要重绘;
    • shouldRelayout:是否需要重新布局。

    其中,我们平时使用的时候,一般会使用到paintChildren以及shouldRepaint两个方法。

    1.5.2 源码

    我们先来看一下Flow的布局代码

    1. Size _getSize(BoxConstraints constraints) {
    2. assert(constraints.debugAssertIsValid());
    3. return constraints.constrain(_delegate.getSize(constraints));
    4. }
    5. @override
    6. void performLayout() {
    7. size = _getSize(constraints);
    8. int i = 0;
    9. _randomAccessChildren.clear();
    10. RenderBox child = firstChild;
    11. while (child != null) {
    12. _randomAccessChildren.add(child);
    13. final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);
    14. child.layout(innerConstraints, parentUsesSize: true);
    15. final FlowParentData childParentData = child.parentData;
    16. childParentData.offset = Offset.zero;
    17. child = childParentData.nextSibling;
    18. i += 1;
    19. }
    20. }

    可以看到Flow尺寸的取值,直接来自于delegate的getSize方法。对于每一个child,则是将delegate中的getConstraintsForChild设置的约束条件,设置在child上。

    Flow布局上的表现,受Delegate中getSize以及getConstraintsForChild两个方法的影响。第一个方法设置其尺寸,第二个方法设置其children的布局约束条件。

    接下来我们来看一下其绘制方法。

    1. void _paintWithDelegate(PaintingContext context, Offset offset) {
    2. _lastPaintOrder.clear();
    3. _paintingContext = context;
    4. _paintingOffset = offset;
    5. for (RenderBox child in _randomAccessChildren) {
    6. final FlowParentData childParentData = child.parentData;
    7. childParentData._transform = null;
    8. }
    9. try {
    10. _delegate.paintChildren(this);
    11. } finally {
    12. _paintingContext = null;
    13. _paintingOffset = null;
    14. }
    15. }

    它的绘制方法非常的简单,先将上次设置的参数都初始化,然后调用delegate中的paintChildren进行绘制。在paintChildren中会调用paintChild方法去绘制每个child,我们接下来看下其代码。

    1. @override
    2. void paintChild(int i, { Matrix4 transform, double opacity = 1.0 }) {
    3. transform ??= new Matrix4.identity();
    4. final RenderBox child = _randomAccessChildren[i];
    5. final FlowParentData childParentData = child.parentData;
    6. _lastPaintOrder.add(i);
    7. childParentData._transform = transform;
    8. if (opacity == 0.0)
    9. return;
    10. void painter(PaintingContext context, Offset offset) {
    11. context.paintChild(child, offset);
    12. }
    13. if (opacity == 1.0) {
    14. _paintingContext.pushTransform(needsCompositing, _paintingOffset, transform, painter);
    15. } else {
    16. _paintingContext.pushOpacity(_paintingOffset, _getAlphaFromOpacity(opacity), (PaintingContext context, Offset offset) {
    17. context.pushTransform(needsCompositing, offset, transform, painter);
    18. });
    19. }
    20. }

    paitChild函数首先会将transform值设在child上,然后根据opacity值,决定其绘制的表现。

    • 当opacity为0时,只是设置了transform值,这样做是为了让其响应区域跟随调整,虽然不显示出来;
    • 当opacity为1的时候,只是进行Transform操作;
    • 当opacity大于0小于1时,先调整其透明度,再进行Transform操作。

    至于其为什么高效,主要是因为它的布局函数不牵涉到child的布局,而在绘制的时候,则根据delegate中的策略,进行有效的绘制。

    1.6 使用场景

    Flow在一些定制化的流式布局中,有可用场景,但是一般写起来比较复杂,但胜在灵活性以及其高效。

    2. Table

    A widget that uses the table layout algorithm for its children.

    2.1 简介

    每一种移动端布局中都会有一种table布局,这种控件太常见了。至于其表现形式,完全可以借鉴其他移动端的,通俗点讲,就是表格。

    2.2 布局行为

    表格的每一行的高度,由其内容决定,每一列的宽度,则由columnWidths属性单独控制。

    2.3 继承关系

    1. Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > Table

    2.4 示例代码

    1. Table(
    2. columnWidths: const <int, TableColumnWidth>{
    3. 0: FixedColumnWidth(50.0),
    4. 1: FixedColumnWidth(100.0),
    5. 2: FixedColumnWidth(50.0),
    6. 3: FixedColumnWidth(100.0),
    7. },
    8. border: TableBorder.all(color: Colors.red, width: 1.0, style: BorderStyle.solid),
    9. children: const <TableRow>[
    10. TableRow(
    11. children: <Widget>[
    12. Text('A1'),
    13. Text('B1'),
    14. Text('C1'),
    15. Text('D1'),
    16. ],
    17. ),
    18. TableRow(
    19. children: <Widget>[
    20. Text('A2'),
    21. Text('B2'),
    22. Text('C2'),
    23. Text('D2'),
    24. ],
    25. ),
    26. TableRow(
    27. children: <Widget>[
    28. Text('A3'),
    29. Text('B3'),
    30. Text('C3'),
    31. Text('D3'),
    32. ],
    33. ),
    34. ],
    35. )

    一个三行四列的表格,第一三行宽度为50,第二四行宽度为100。

    Table样例

    2.5 源码解析

    构造函数如下:

    1. Table({
    2. Key key,
    3. this.children = const <TableRow>[],
    4. this.columnWidths,
    5. this.defaultColumnWidth = const FlexColumnWidth(1.0),
    6. this.textDirection,
    7. this.border,
    8. this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
    9. this.textBaseline,
    10. })

    2.5.1 属性解析

    columnWidths:设置每一列的宽度。

    defaultColumnWidth:默认的每一列宽度值,默认情况下均分。

    textDirection:文字方向,一般无需考虑。

    border:表格边框。

    defaultVerticalAlignment:每一个cell的垂直方向的alignment。

    总共包含5种:

    • top:被放置在的顶部;
    • middle:垂直居中;
    • bottom:放置在底部;
    • baseline:文本baseline对齐;
    • fill:充满整个cell。

    textBaseline:defaultVerticalAlignment为baseline的时候,会用到这个属性。

    2.5.2 源码

    我们直接来看其布局源码:

    第一步,当行或者列为0的时候,将自身尺寸设为0x0。

    1. if (rows * columns == 0) {
    2. size = constraints.constrain(const Size(0.0, 0.0));
    3. return;
    4. }

    第二步,根据textDirection值,设置方向,一般在阿拉伯语系中,一些文本都是从右往左现实的,平时使用时,不需要去考虑这个属性。

    1. switch (textDirection) {
    2. case TextDirection.rtl:
    3. positions[columns - 1] = 0.0;
    4. for (int x = columns - 2; x >= 0; x -= 1)
    5. positions[x] = positions[x+1] + widths[x+1];
    6. _columnLefts = positions.reversed;
    7. tableWidth = positions.first + widths.first;
    8. break;
    9. case TextDirection.ltr:
    10. positions[0] = 0.0;
    11. for (int x = 1; x < columns; x += 1)
    12. positions[x] = positions[x-1] + widths[x-1];
    13. _columnLefts = positions;
    14. tableWidth = positions.last + widths.last;
    15. break;
    16. }

    第三步,设置每一个cell的尺寸。

    1. for (int x = 0; x < columns; x += 1) {
    2. final int xy = x + y * columns;
    3. final RenderBox child = _children[xy];
    4. if (child != null) {
    5. final TableCellParentData childParentData = child.parentData;
    6. childParentData.x = x;
    7. childParentData.y = y;
    8. switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
    9. case TableCellVerticalAlignment.baseline:
    10. child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
    11. final double childBaseline = child.getDistanceToBaseline(textBaseline, onlyReal: true);
    12. if (childBaseline != null) {
    13. beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline);
    14. afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline);
    15. baselines[x] = childBaseline;
    16. haveBaseline = true;
    17. } else {
    18. rowHeight = math.max(rowHeight, child.size.height);
    19. childParentData.offset = new Offset(positions[x], rowTop);
    20. }
    21. break;
    22. case TableCellVerticalAlignment.top:
    23. case TableCellVerticalAlignment.middle:
    24. case TableCellVerticalAlignment.bottom:
    25. child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
    26. rowHeight = math.max(rowHeight, child.size.height);
    27. break;
    28. case TableCellVerticalAlignment.fill:
    29. break;
    30. }
    31. }
    32. }

    第四步,如果有baseline则进行相关设置。

    1. if (haveBaseline) {
    2. if (y == 0)
    3. _baselineDistance = beforeBaselineDistance;
    4. rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance);
    5. }

    第五步,根据alignment,调整child的位置。

    1. for (int x = 0; x < columns; x += 1) {
    2. final int xy = x + y * columns;
    3. final RenderBox child = _children[xy];
    4. if (child != null) {
    5. final TableCellParentData childParentData = child.parentData;
    6. switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
    7. case TableCellVerticalAlignment.baseline:
    8. if (baselines[x] != null)
    9. childParentData.offset = new Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]);
    10. break;
    11. case TableCellVerticalAlignment.top:
    12. childParentData.offset = new Offset(positions[x], rowTop);
    13. break;
    14. case TableCellVerticalAlignment.middle:
    15. childParentData.offset = new Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0);
    16. break;
    17. case TableCellVerticalAlignment.bottom:
    18. childParentData.offset = new Offset(positions[x], rowTop + rowHeight - child.size.height);
    19. break;
    20. case TableCellVerticalAlignment.fill:
    21. child.layout(new BoxConstraints.tightFor(width: widths[x], height: rowHeight));
    22. childParentData.offset = new Offset(positions[x], rowTop);
    23. break;
    24. }
    25. }
    26. }

    最后一步,则是根据每一行的宽度以及每一列的高度,设置Table的尺寸。

    1. size = constraints.constrain(new Size(tableWidth, rowTop));

    最后梳理一下整个的布局流程:

    • 当行或者列为0的时候,将自身尺寸设为0x0;
    • 根据textDirection进行相关设置;
    • 设置cell的尺寸;
    • 如果设置了baseline,则进行相关设置;
    • 根据alignment设置cell垂直方向的位置;
    • 设置Table的尺寸。

    如果经常关注系列文章的读者,可能会发现,布局控件的布局流程基本上跟上述流程是相似的。

    2.6 使用场景

    在一些需要表格展示的场景中,可以使用Table控件。

    3. Wrap

    A widget that displays its children in multiple horizontal or vertical runs.

    3.1 简介

    看简介,其实Wrap实现的效果,Flow可以很轻松,而且可以更加灵活的实现出来。

    3.2 布局行为

    Flow可以很轻易的实现Wrap的效果,但是Wrap更多的是在使用了Flex中的一些概念,某种意义上说是跟Row、Column更加相似的。

    单行的Wrap跟Row表现几乎一致,单列的Wrap则跟Row表现几乎一致。但Row与Column都是单行单列的,Wrap则突破了这个限制,mainAxis上空间不足时,则向crossAxis上去扩展显示。

    从效率上讲,Flow肯定会比Wrap高,但是Wrap使用起来会方便一些。

    3.3 继承关系

    1. Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Wrap

    从继承关系上看,Wrap与Flow都是继承自MultiChildRenderObjectWidget,Flow可以实现Wrap的效果,但是两者却是单独实现的,说明两者有很大的不同。

    3.4 示例代码

    1. Wrap(
    2. spacing: 8.0, // gap between adjacent chips
    3. runSpacing: 4.0, // gap between lines
    4. children: <Widget>[
    5. Chip(
    6. avatar: CircleAvatar(
    7. backgroundColor: Colors.blue.shade900, child: new Text('AH', style: TextStyle(fontSize: 10.0),)),
    8. label: Text('Hamilton'),
    9. ),
    10. Chip(
    11. avatar: CircleAvatar(
    12. backgroundColor: Colors.blue.shade900, child: new Text('ML', style: TextStyle(fontSize: 10.0),)),
    13. label: Text('Lafayette'),
    14. ),
    15. Chip(
    16. avatar: CircleAvatar(
    17. backgroundColor: Colors.blue.shade900, child: new Text('HM', style: TextStyle(fontSize: 10.0),)),
    18. label: Text('Mulligan'),
    19. ),
    20. Chip(
    21. avatar: CircleAvatar(
    22. backgroundColor: Colors.blue.shade900, child: new Text('JL', style: TextStyle(fontSize: 10.0),)),
    23. label: Text('Laurens'),
    24. ),
    25. ],
    26. )

    示例代码直接使用的官方文档上的,效果跟Flow的例子中相似。

    Wrap样例

    3.5 源码解析

    构造函数如下:

    1. Wrap({
    2. Key key,
    3. this.direction = Axis.horizontal,
    4. this.alignment = WrapAlignment.start,
    5. this.spacing = 0.0,
    6. this.runAlignment = WrapAlignment.start,
    7. this.runSpacing = 0.0,
    8. this.crossAxisAlignment = WrapCrossAlignment.start,
    9. this.textDirection,
    10. this.verticalDirection = VerticalDirection.down,
    11. List<Widget> children = const <Widget>[],
    12. })

    3.5.1 属性解析

    direction:主轴(mainAxis)的方向,默认为水平。

    alignment:主轴方向上的对齐方式,默认为start。

    spacing:主轴方向上的间距。

    runAlignment:run的对齐方式。run可以理解为新的行或者列,如果是水平方向布局的话,run可以理解为新的一行。

    runSpacing:run的间距。

    crossAxisAlignment:交叉轴(crossAxis)方向上的对齐方式。

    textDirection:文本方向。

    verticalDirection:定义了children摆放顺序,默认是down,见Flex相关属性介绍。

    3.5.2 源码

    我们来看下其布局代码。

    第一步,如果第一个child为null,则将其设置为最小尺寸。

    1. RenderBox child = firstChild;
    2. if (child == null) {
    3. size = constraints.smallest;
    4. return;
    5. }

    第二步,根据direction、textDirection以及verticalDirection属性,计算出相关的mainAxis、crossAxis是否需要调整方向,以及主轴方向上的限制。

    1. double mainAxisLimit = 0.0;
    2. bool flipMainAxis = false;
    3. bool flipCrossAxis = false;
    4. switch (direction) {
    5. case Axis.horizontal:
    6. childConstraints = new BoxConstraints(maxWidth: constraints.maxWidth);
    7. mainAxisLimit = constraints.maxWidth;
    8. if (textDirection == TextDirection.rtl)
    9. flipMainAxis = true;
    10. if (verticalDirection == VerticalDirection.up)
    11. flipCrossAxis = true;
    12. break;
    13. case Axis.vertical:
    14. childConstraints = new BoxConstraints(maxHeight: constraints.maxHeight);
    15. mainAxisLimit = constraints.maxHeight;
    16. if (verticalDirection == VerticalDirection.up)
    17. flipMainAxis = true;
    18. if (textDirection == TextDirection.rtl)
    19. flipCrossAxis = true;
    20. break;
    21. }

    第三步,计算出主轴以及交叉轴的区域大小。

    1. while (child != null) {
    2. child.layout(childConstraints, parentUsesSize: true);
    3. final double childMainAxisExtent = _getMainAxisExtent(child);
    4. final double childCrossAxisExtent = _getCrossAxisExtent(child);
    5. if (childCount > 0 && runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit) {
    6. mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
    7. crossAxisExtent += runCrossAxisExtent;
    8. if (runMetrics.isNotEmpty)
    9. crossAxisExtent += runSpacing;
    10. runMetrics.add(new _RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount));
    11. runMainAxisExtent = 0.0;
    12. runCrossAxisExtent = 0.0;
    13. childCount = 0;
    14. }
    15. runMainAxisExtent += childMainAxisExtent;
    16. if (childCount > 0)
    17. runMainAxisExtent += spacing;
    18. runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent);
    19. childCount += 1;
    20. final WrapParentData childParentData = child.parentData;
    21. childParentData._runIndex = runMetrics.length;
    22. child = childParentData.nextSibling;
    23. }

    第四步,根据direction设置Wrap的尺寸。

    1. switch (direction) {
    2. case Axis.horizontal:
    3. size = constraints.constrain(new Size(mainAxisExtent, crossAxisExtent));
    4. containerMainAxisExtent = size.width;
    5. containerCrossAxisExtent = size.height;
    6. break;
    7. case Axis.vertical:
    8. size = constraints.constrain(new Size(crossAxisExtent, mainAxisExtent));
    9. containerMainAxisExtent = size.height;
    10. containerCrossAxisExtent = size.width;
    11. break;
    12. }

    第五步,根据runAlignment计算出每一个run之间的距离,几种属性的差异,之前文章介绍过,在此就不做详细阐述。

    1. final double crossAxisFreeSpace = math.max(0.0, containerCrossAxisExtent - crossAxisExtent);
    2. double runLeadingSpace = 0.0;
    3. double runBetweenSpace = 0.0;
    4. switch (runAlignment) {
    5. case WrapAlignment.start:
    6. break;
    7. case WrapAlignment.end:
    8. runLeadingSpace = crossAxisFreeSpace;
    9. break;
    10. case WrapAlignment.center:
    11. runLeadingSpace = crossAxisFreeSpace / 2.0;
    12. break;
    13. case WrapAlignment.spaceBetween:
    14. runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0;
    15. break;
    16. case WrapAlignment.spaceAround:
    17. runBetweenSpace = crossAxisFreeSpace / runCount;
    18. runLeadingSpace = runBetweenSpace / 2.0;
    19. break;
    20. case WrapAlignment.spaceEvenly:
    21. runBetweenSpace = crossAxisFreeSpace / (runCount + 1);
    22. runLeadingSpace = runBetweenSpace;
    23. break;
    24. }

    第六步,根据alignment计算出每一个run中child的主轴方向上的间距。

    1. switch (alignment) {
    2. case WrapAlignment.start:
    3. break;
    4. case WrapAlignment.end:
    5. childLeadingSpace = mainAxisFreeSpace;
    6. break;
    7. case WrapAlignment.center:
    8. childLeadingSpace = mainAxisFreeSpace / 2.0;
    9. break;
    10. case WrapAlignment.spaceBetween:
    11. childBetweenSpace = childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0;
    12. break;
    13. case WrapAlignment.spaceAround:
    14. childBetweenSpace = mainAxisFreeSpace / childCount;
    15. childLeadingSpace = childBetweenSpace / 2.0;
    16. break;
    17. case WrapAlignment.spaceEvenly:
    18. childBetweenSpace = mainAxisFreeSpace / (childCount + 1);
    19. childLeadingSpace = childBetweenSpace;
    20. break;
    21. }

    最后一步,调整child的位置。

    1. while (child != null) {
    2. final WrapParentData childParentData = child.parentData;
    3. if (childParentData._runIndex != i)
    4. break;
    5. final double childMainAxisExtent = _getMainAxisExtent(child);
    6. final double childCrossAxisExtent = _getCrossAxisExtent(child);
    7. final double childCrossAxisOffset = _getChildCrossAxisOffset(flipCrossAxis, runCrossAxisExtent, childCrossAxisExtent);
    8. if (flipMainAxis)
    9. childMainPosition -= childMainAxisExtent;
    10. childParentData.offset = _getOffset(childMainPosition, crossAxisOffset + childCrossAxisOffset);
    11. if (flipMainAxis)
    12. childMainPosition -= childBetweenSpace;
    13. else
    14. childMainPosition += childMainAxisExtent + childBetweenSpace;
    15. child = childParentData.nextSibling;
    16. }
    17. if (flipCrossAxis)
    18. crossAxisOffset -= runBetweenSpace;
    19. else
    20. crossAxisOffset += runCrossAxisExtent + runBetweenSpace;

    我们大致梳理一下布局的流程。

    • 如果第一个child为null,则将Wrap设置为最小尺寸,布局结束;
    • 根据direction、textDirection以及verticalDirection属性,计算出mainAxis、crossAxis是否需要调整方向;
    • 计算出主轴以及交叉轴的区域大小;
    • 根据direction设置Wrap的尺寸;
    • 根据runAlignment计算出每一个run之间的距离;
    • 根据alignment计算出每一个run中child的主轴方向上的间距
    • 调整每一个child的位置。

    3.6 使用场景

    对于一些需要按宽度或者高度,让child自动换行布局的场景,可以使用,但是Wrap可以满足的场景,Flow一定可以实现,只不过会复杂很多,但是相对的会灵活以及高效很多。

    4. 后话

    笔者建了一个Flutter学习相关的项目,Github地址,里面包含了笔者写的关于Flutter学习相关的一些文章,会定期更新,也会上传一些学习Demo,欢迎大家关注。

    5. 参考

    1. Flow class
    2. Table class
    3. Wrap class