• Flutter 布局(七)- Row、Column详解
    • 1. Row
      • 1.1 简介
      • 1.2 布局行为
      • 1.3 继承关系
      • 1.4 示例代码
      • 1.5 源码解析
        • 1.5.1 属性解析
        • 1.5.2 源码
      • 1.6 使用场景
    • 2. Column
    • 3. 后话
    • 4. 参考

    Flutter 布局(七)- Row、Column详解

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

    1. Row

    A widget that displays its children in a horizontal array.

    1.1 简介

    在Flutter中非常常见的一个多子节点控件,将children排列成一行。估计是借鉴了Web中Flex布局,所以很多属性和表现,都跟其相似。但是注意一点,自身不带滚动属性,如果超出了一行,在debug下面则会显示溢出的提示。

    1.2 布局行为

    Row的布局有六个步骤,这种布局表现来自Flex(Row和Column的父类):

    1. 首先按照不受限制的主轴(main axis)约束条件,对flex为null或者为0的child进行布局,然后按照交叉轴( cross axis)的约束,对child进行调整;
    2. 按照不为空的flex值,将主轴方向上剩余的空间分成相应的几等分;
    3. 对上述步骤flex值不为空的child,在交叉轴方向进行调整,在主轴方向使用最大约束条件,让其占满步骤2所分得的空间;
    4. Flex交叉轴的范围取自子节点的最大交叉轴;
    5. 主轴Flex的值是由mainAxisSize属性决定的,其中MainAxisSize可以取max、min以及具体的value值;
    6. 每一个child的位置是由mainAxisAlignment以及crossAxisAlignment所决定。

    Row的布局行为表面上看有这么多个步骤,其实也还算是简单,可以完全参照web中的Flex布局,包括主轴、交叉轴等概念。

    Flex

    1.3 继承关系

    1. Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Row

    Row以及Column都是Flex的子类,它们的具体实现也都是由Flex完成,只是参数不同。

    1.4 示例代码

    1. Row(
    2. children: <Widget>[
    3. Expanded(
    4. child: Container(
    5. color: Colors.red,
    6. padding: EdgeInsets.all(5.0),
    7. ),
    8. flex: 1,
    9. ),
    10. Expanded(
    11. child: Container(
    12. color: Colors.yellow,
    13. padding: EdgeInsets.all(5.0),
    14. ),
    15. flex: 2,
    16. ),
    17. Expanded(
    18. child: Container(
    19. color: Colors.blue,
    20. padding: EdgeInsets.all(5.0),
    21. ),
    22. flex: 1,
    23. ),
    24. ],
    25. )

    一个很简单的例子,使用Expanded控件,将一行的宽度分成四个等分,第一、三个child占1/4的区域,第二个child占1/2区域,由flex属性控制。

    1.5 源码解析

    构造函数如下:

    1. Row({
    2. Key key,
    3. MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
    4. MainAxisSize mainAxisSize = MainAxisSize.max,
    5. CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
    6. TextDirection textDirection,
    7. VerticalDirection verticalDirection = VerticalDirection.down,
    8. TextBaseline textBaseline,
    9. List<Widget> children = const <Widget>[],
    10. })

    1.5.1 属性解析

    MainAxisAlignment:主轴方向上的对齐方式,会对child的位置起作用,默认是start。

    其中MainAxisAlignment枚举值:

    • center:将children放置在主轴的中心;
    • end:将children放置在主轴的末尾;
    • spaceAround:将主轴方向上的空白区域均分,使得children之间的空白区域相等,但是首尾child的空白区域为1/2;
    • spaceBetween:将主轴方向上的空白区域均分,使得children之间的空白区域相等,首尾child都靠近首尾,没有间隙;
    • spaceEvenly:将主轴方向上的空白区域均分,使得children之间的空白区域相等,包括首尾child;
    • start:将children放置在主轴的起点;

    其中spaceAround、spaceBetween以及spaceEvenly的区别,就是对待首尾child的方式。其距离首尾的距离分别是空白区域的1/2、0、1。

    MainAxisSize:在主轴方向占有空间的值,默认是max。

    MainAxisSize的取值有两种:

    • max:根据传入的布局约束条件,最大化主轴方向的可用空间;
    • min:与max相反,是最小化主轴方向的可用空间;

    CrossAxisAlignment:children在交叉轴方向的对齐方式,与MainAxisAlignment略有不同。

    CrossAxisAlignment枚举值有如下几种:

    • baseline:在交叉轴方向,使得children的baseline对齐;
    • center:children在交叉轴上居中展示;
    • end:children在交叉轴上末尾展示;
    • start:children在交叉轴上起点处展示;
    • stretch:让children填满交叉轴方向;

    TextDirection:阿拉伯语系的兼容设置,一般无需处理。

    VerticalDirection:定义了children摆放顺序,默认是down。

    VerticalDirection枚举值有两种:

    • down:从top到bottom进行布局;
    • up:从bottom到top进行布局。

    top对应Row以及Column的话,就是左边和顶部,bottom的话,则是右边和底部。

    TextBaseline:使用的TextBaseline的方式,有两种,前面已经介绍过。

    1.5.2 源码

    Row以及Column的源代码就一个构造函数,具体的实现全部在它们的父类Flex中。

    关于Flex的构造函数

    1. Flex({
    2. Key key,
    3. @required this.direction,
    4. this.mainAxisAlignment = MainAxisAlignment.start,
    5. this.mainAxisSize = MainAxisSize.max,
    6. this.crossAxisAlignment = CrossAxisAlignment.center,
    7. this.textDirection,
    8. this.verticalDirection = VerticalDirection.down,
    9. this.textBaseline,
    10. List<Widget> children = const <Widget>[],
    11. })

    可以看出,Flex的构造函数就比Row和Column的多了一个参数。Row跟Column的区别,正是这个direction参数的不同。当为Axis.horizontal的时候,则是Row,当为Axis.vertical的时候,则是Column。

    我们来看下Flex的布局函数,由于布局函数比较多,因此分段来讲解:

    1. while (child != null) {
    2. final FlexParentData childParentData = child.parentData;
    3. totalChildren++;
    4. final int flex = _getFlex(child);
    5. if (flex > 0) {
    6. totalFlex += childParentData.flex;
    7. lastFlexChild = child;
    8. } else {
    9. BoxConstraints innerConstraints;
    10. if (crossAxisAlignment == CrossAxisAlignment.stretch) {
    11. switch (_direction) {
    12. case Axis.horizontal:
    13. innerConstraints = new BoxConstraints(minHeight: constraints.maxHeight,
    14. maxHeight: constraints.maxHeight);
    15. break;
    16. case Axis.vertical:
    17. innerConstraints = new BoxConstraints(minWidth: constraints.maxWidth,
    18. maxWidth: constraints.maxWidth);
    19. break;
    20. }
    21. } else {
    22. switch (_direction) {
    23. case Axis.horizontal:
    24. innerConstraints = new BoxConstraints(maxHeight: constraints.maxHeight);
    25. break;
    26. case Axis.vertical:
    27. innerConstraints = new BoxConstraints(maxWidth: constraints.maxWidth);
    28. break;
    29. }
    30. }
    31. child.layout(innerConstraints, parentUsesSize: true);
    32. allocatedSize += _getMainSize(child);
    33. crossSize = math.max(crossSize, _getCrossSize(child));
    34. }
    35. child = childParentData.nextSibling;
    36. }

    上面这段代码,我把中间的一些assert以及错误信息之类的代码剔除了,不影响实际的理解。

    在布局的开始,首先会遍历一遍child,遍历的作用有两点:

    • 对于存在flex值的child,计算出flex的和,找到最后一个包含flex值的child。找到这个child,是因为主轴对齐方式,可能会对它的位置做调整,需要找出来;
    • 对于不包含flex的child,根据交叉轴方向的设置,对child进行调整。
    1. final double freeSpace = math.max(0.0, (canFlex ? maxMainSize : 0.0) - allocatedSize);
    2. if (totalFlex > 0 || crossAxisAlignment == CrossAxisAlignment.baseline) {
    3. final double spacePerFlex = canFlex && totalFlex > 0 ? (freeSpace / totalFlex) : double.nan;
    4. child = firstChild;
    5. while (child != null) {
    6. final int flex = _getFlex(child);
    7. if (flex > 0) {
    8. final double maxChildExtent = canFlex ? (child == lastFlexChild ? (freeSpace - allocatedFlexSpace) : spacePerFlex * flex) : double.infinity;
    9. double minChildExtent;
    10. switch (_getFit(child)) {
    11. case FlexFit.tight:
    12. assert(maxChildExtent < double.infinity);
    13. minChildExtent = maxChildExtent;
    14. break;
    15. case FlexFit.loose:
    16. minChildExtent = 0.0;
    17. break;
    18. }
    19. BoxConstraints innerConstraints;
    20. if (crossAxisAlignment == CrossAxisAlignment.stretch) {
    21. switch (_direction) {
    22. case Axis.horizontal:
    23. innerConstraints = new BoxConstraints(minWidth: minChildExtent,
    24. maxWidth: maxChildExtent,
    25. minHeight: constraints.maxHeight,
    26. maxHeight: constraints.maxHeight);
    27. break;
    28. case Axis.vertical:
    29. innerConstraints = new BoxConstraints(minWidth: constraints.maxWidth,
    30. maxWidth: constraints.maxWidth,
    31. minHeight: minChildExtent,
    32. maxHeight: maxChildExtent);
    33. break;
    34. }
    35. } else {
    36. switch (_direction) {
    37. case Axis.horizontal:
    38. innerConstraints = new BoxConstraints(minWidth: minChildExtent,
    39. maxWidth: maxChildExtent,
    40. maxHeight: constraints.maxHeight);
    41. break;
    42. case Axis.vertical:
    43. innerConstraints = new BoxConstraints(maxWidth: constraints.maxWidth,
    44. minHeight: minChildExtent,
    45. maxHeight: maxChildExtent);
    46. break;
    47. }
    48. }
    49. child.layout(innerConstraints, parentUsesSize: true);
    50. final double childSize = _getMainSize(child);
    51. allocatedSize += childSize;
    52. allocatedFlexSpace += maxChildExtent;
    53. crossSize = math.max(crossSize, _getCrossSize(child));
    54. }
    55. if (crossAxisAlignment == CrossAxisAlignment.baseline) {
    56. final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
    57. if (distance != null)
    58. maxBaselineDistance = math.max(maxBaselineDistance, distance);
    59. }
    60. final FlexParentData childParentData = child.parentData;
    61. child = childParentData.nextSibling;
    62. }
    63. }

    上面的代码段所做的事情也有两点:

    • 为包含flex的child分配剩余的空间

    对于每份flex所对应的空间大小,它的计算方式如下:

    1. final double freeSpace = math.max(0.0, (canFlex ? maxMainSize : 0.0) - allocatedSize);
    2. final double spacePerFlex = canFlex && totalFlex > 0 ? (freeSpace / totalFlex) : double.nan;

    其中,allocatedSize是不包含flex所占用的空间。当每一份flex所占用的空间计算出来后,则根据交叉轴的设置,对包含flex的child进行调整。

    • 计算出baseline值

    如果交叉轴的对齐方式为baseline,则计算出最大的baseline值,将其作为整体的baseline值。

    1. switch (_mainAxisAlignment) {
    2. case MainAxisAlignment.start:
    3. leadingSpace = 0.0;
    4. betweenSpace = 0.0;
    5. break;
    6. case MainAxisAlignment.end:
    7. leadingSpace = remainingSpace;
    8. betweenSpace = 0.0;
    9. break;
    10. case MainAxisAlignment.center:
    11. leadingSpace = remainingSpace / 2.0;
    12. betweenSpace = 0.0;
    13. break;
    14. case MainAxisAlignment.spaceBetween:
    15. leadingSpace = 0.0;
    16. betweenSpace = totalChildren > 1 ? remainingSpace / (totalChildren - 1) : 0.0;
    17. break;
    18. case MainAxisAlignment.spaceAround:
    19. betweenSpace = totalChildren > 0 ? remainingSpace / totalChildren : 0.0;
    20. leadingSpace = betweenSpace / 2.0;
    21. break;
    22. case MainAxisAlignment.spaceEvenly:
    23. betweenSpace = totalChildren > 0 ? remainingSpace / (totalChildren + 1) : 0.0;
    24. leadingSpace = betweenSpace;
    25. break;
    26. }

    然后,就是将child在主轴方向上按照设置的对齐方式,进行位置调整。上面代码就是计算前后空白区域值的过程,可以看出spaceBetween、spaceAround以及spaceEvenly的差别。

    1. double childMainPosition = flipMainAxis ? actualSize - leadingSpace : leadingSpace;
    2. child = firstChild;
    3. while (child != null) {
    4. final FlexParentData childParentData = child.parentData;
    5. double childCrossPosition;
    6. switch (_crossAxisAlignment) {
    7. case CrossAxisAlignment.start:
    8. case CrossAxisAlignment.end:
    9. childCrossPosition = _startIsTopLeft(flipAxis(direction), textDirection, verticalDirection)
    10. == (_crossAxisAlignment == CrossAxisAlignment.start)
    11. ? 0.0
    12. : crossSize - _getCrossSize(child);
    13. break;
    14. case CrossAxisAlignment.center:
    15. childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0;
    16. break;
    17. case CrossAxisAlignment.stretch:
    18. childCrossPosition = 0.0;
    19. break;
    20. case CrossAxisAlignment.baseline:
    21. childCrossPosition = 0.0;
    22. if (_direction == Axis.horizontal) {
    23. assert(textBaseline != null);
    24. final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
    25. if (distance != null)
    26. childCrossPosition = maxBaselineDistance - distance;
    27. }
    28. break;
    29. }
    30. if (flipMainAxis)
    31. childMainPosition -= _getMainSize(child);
    32. switch (_direction) {
    33. case Axis.horizontal:
    34. childParentData.offset = new Offset(childMainPosition, childCrossPosition);
    35. break;
    36. case Axis.vertical:
    37. childParentData.offset = new Offset(childCrossPosition, childMainPosition);
    38. break;
    39. }
    40. if (flipMainAxis) {
    41. childMainPosition -= betweenSpace;
    42. } else {
    43. childMainPosition += _getMainSize(child) + betweenSpace;
    44. }
    45. child = childParentData.nextSibling;
    46. }

    最后,则是根据交叉轴的对齐方式设置,对child进行位置调整,到此,布局结束。

    我们可以顺一下整体的流程:

    • 计算出flex的总和,并找到最后一个设置了flex的child;
    • 对不包含flex的child,根据交叉轴对齐方式,对齐进行调整,并计算出主轴方向上所占区域大小;
    • 计算出每一份flex所占用的空间,并根据交叉轴对齐方式,对包含flex的child进行调整;
    • 如果交叉轴设置为baseline对齐,则计算出整体的baseline值;
    • 按照主轴对齐方式,对child进行调整;
    • 最后,根据交叉轴对齐方式,对所有child位置进行调整,完成布局。

    1.6 使用场景

    Row和Column都是非常常用的布局控件。一般情况下,比方说需要将控件在一行或者一列显示的时候,都可以使用。但并不是说只能使用Row或者Column去布局,也可以使用Stack,看具体的场景选择。

    2. Column

    在讲解Row的时候,其实是按照Flex的一些布局行为来进行的,包括源码分析,也都是在用Flex进行分析的。Row和Column都是Flex的子类,只是direction参数不同。Column各方面同Row,因此在这里不再另行讲解。

    在讲解Flex的时候,也说过是参照了web的Flex布局,如果有相关开发经验的同学,完全可以参照着去理解,这样子更容易去理解它们的用法和原理。

    3. 后话

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

    4. 参考

    1. Row class
    2. Column class
    3. MainAxisAlignment enum
    4. CrossAxisAlignment enum
    5. MainAxisSize enum
    6. VerticalDirection enum