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 繼承關系

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

Row以及Column都是Flex的子類,它們的具體實現也都是由Flex完成,只是參數不同。

1.4 示例代碼

Row(
  children: <Widget>[
    Expanded(
      child: Container(
        color: Colors.red,
        padding: EdgeInsets.all(5.0),
      ),
      flex: 1,
    ),
    Expanded(
      child: Container(
        color: Colors.yellow,
        padding: EdgeInsets.all(5.0),
      ),
      flex: 2,
    ),
    Expanded(
      child: Container(
        color: Colors.blue,
        padding: EdgeInsets.all(5.0),
      ),
      flex: 1,
    ),
  ],
)

一個很簡單的例子,使用Expanded控件,將一行的寬度分成四個等分,第一、三個child占1/4的區域,第二個child占1/2區域,由flex屬性控制。

1.5 源碼解析

構造函數如下:

Row({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline textBaseline,
  List<Widget> children = const <Widget>[],
})

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的構造函數

Flex({
  Key key,
  @required this.direction,
  this.mainAxisAlignment = MainAxisAlignment.start,
  this.mainAxisSize = MainAxisSize.max,
  this.crossAxisAlignment = CrossAxisAlignment.center,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  this.textBaseline,
  List<Widget> children = const <Widget>[],
})

可以看出,Flex的構造函數就比Row和Column的多了一個參數。Row跟Column的區別,正是這個direction參數的不同。當為Axis.horizontal的時候,則是Row,當為Axis.vertical的時候,則是Column。

我們來看下Flex的布局函數,由于布局函數比較多,因此分段來講解:

while (child != null) {
  final FlexParentData childParentData = child.parentData;
  totalChildren++;
  final int flex = _getFlex(child);
  if (flex > 0) {
    totalFlex += childParentData.flex;
    lastFlexChild = child;
  } else {
    BoxConstraints innerConstraints;
    if (crossAxisAlignment == CrossAxisAlignment.stretch) {
      switch (_direction) {
        case Axis.horizontal:
          innerConstraints = new BoxConstraints(minHeight: constraints.maxHeight,
                                                maxHeight: constraints.maxHeight);
          break;
        case Axis.vertical:
          innerConstraints = new BoxConstraints(minWidth: constraints.maxWidth,
                                                maxWidth: constraints.maxWidth);
          break;
      }
    } else {
      switch (_direction) {
        case Axis.horizontal:
          innerConstraints = new BoxConstraints(maxHeight: constraints.maxHeight);
          break;
        case Axis.vertical:
          innerConstraints = new BoxConstraints(maxWidth: constraints.maxWidth);
          break;
      }
    }
    child.layout(innerConstraints, parentUsesSize: true);
    allocatedSize += _getMainSize(child);
    crossSize = math.max(crossSize, _getCrossSize(child));
  }
  child = childParentData.nextSibling;
}

上面這段代碼,我把中間的一些assert以及錯誤信息之類的代碼剔除了,不影響實際的理解。

在布局的開始,首先會遍歷一遍child,遍歷的作用有兩點:

  • 對于存在flex值的child,計算出flex的和,找到最后一個包含flex值的child。找到這個child,是因為主軸對齊方式,可能會對它的位置做調整,需要找出來;
  • 對于不包含flex的child,根據交叉軸方向的設置,對child進行調整。
final double freeSpace = math.max(0.0, (canFlex ? maxMainSize : 0.0) - allocatedSize);
if (totalFlex > 0 || crossAxisAlignment == CrossAxisAlignment.baseline) {
  final double spacePerFlex = canFlex && totalFlex > 0 ? (freeSpace / totalFlex) : double.nan;
  child = firstChild;
  while (child != null) {
    final int flex = _getFlex(child);
    if (flex > 0) {
      final double maxChildExtent = canFlex ? (child == lastFlexChild ? (freeSpace - allocatedFlexSpace) : spacePerFlex * flex) : double.infinity;
      double minChildExtent;
      switch (_getFit(child)) {
        case FlexFit.tight:
          assert(maxChildExtent < double.infinity);
          minChildExtent = maxChildExtent;
          break;
        case FlexFit.loose:
          minChildExtent = 0.0;
          break;
      }
      BoxConstraints innerConstraints;
      if (crossAxisAlignment == CrossAxisAlignment.stretch) {
        switch (_direction) {
          case Axis.horizontal:
            innerConstraints = new BoxConstraints(minWidth: minChildExtent,
                                                  maxWidth: maxChildExtent,
                                                  minHeight: constraints.maxHeight,
                                                  maxHeight: constraints.maxHeight);
            break;
          case Axis.vertical:
            innerConstraints = new BoxConstraints(minWidth: constraints.maxWidth,
                                                  maxWidth: constraints.maxWidth,
                                                  minHeight: minChildExtent,
                                                  maxHeight: maxChildExtent);
            break;
        }
      } else {
        switch (_direction) {
          case Axis.horizontal:
            innerConstraints = new BoxConstraints(minWidth: minChildExtent,
                                                  maxWidth: maxChildExtent,
                                                  maxHeight: constraints.maxHeight);
            break;
          case Axis.vertical:
            innerConstraints = new BoxConstraints(maxWidth: constraints.maxWidth,
                                                  minHeight: minChildExtent,
                                                  maxHeight: maxChildExtent);
            break;
        }
      }
      child.layout(innerConstraints, parentUsesSize: true);
      final double childSize = _getMainSize(child);
      allocatedSize += childSize;
      allocatedFlexSpace += maxChildExtent;
      crossSize = math.max(crossSize, _getCrossSize(child));
    }
    if (crossAxisAlignment == CrossAxisAlignment.baseline) {
      final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
      if (distance != null)
        maxBaselineDistance = math.max(maxBaselineDistance, distance);
    }
    final FlexParentData childParentData = child.parentData;
    child = childParentData.nextSibling;
  }
}

上面的代碼段所做的事情也有兩點:

  • 為包含flex的child分配剩余的空間

對于每份flex所對應的空間大小,它的計算方式如下:

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

其中,allocatedSize是不包含flex所占用的空間。當每一份flex所占用的空間計算出來后,則根據交叉軸的設置,對包含flex的child進行調整。

  • 計算出baseline值

如果交叉軸的對齊方式為baseline,則計算出最大的baseline值,將其作為整體的baseline值。

switch (_mainAxisAlignment) {
  case MainAxisAlignment.start:
    leadingSpace = 0.0;
    betweenSpace = 0.0;
    break;
  case MainAxisAlignment.end:
    leadingSpace = remainingSpace;
    betweenSpace = 0.0;
    break;
  case MainAxisAlignment.center:
    leadingSpace = remainingSpace / 2.0;
    betweenSpace = 0.0;
    break;
  case MainAxisAlignment.spaceBetween:
    leadingSpace = 0.0;
    betweenSpace = totalChildren > 1 ? remainingSpace / (totalChildren - 1) : 0.0;
    break;
  case MainAxisAlignment.spaceAround:
    betweenSpace = totalChildren > 0 ? remainingSpace / totalChildren : 0.0;
    leadingSpace = betweenSpace / 2.0;
    break;
  case MainAxisAlignment.spaceEvenly:
    betweenSpace = totalChildren > 0 ? remainingSpace / (totalChildren + 1) : 0.0;
    leadingSpace = betweenSpace;
    break;
}

然后,就是將child在主軸方向上按照設置的對齊方式,進行位置調整。上面代碼就是計算前后空白區域值的過程,可以看出spaceBetween、spaceAround以及spaceEvenly的差別。

double childMainPosition = flipMainAxis ? actualSize - leadingSpace : leadingSpace;
child = firstChild;
while (child != null) {
  final FlexParentData childParentData = child.parentData;
  double childCrossPosition;
  switch (_crossAxisAlignment) {
    case CrossAxisAlignment.start:
    case CrossAxisAlignment.end:
      childCrossPosition = _startIsTopLeft(flipAxis(direction), textDirection, verticalDirection)
                           == (_crossAxisAlignment == CrossAxisAlignment.start)
                         ? 0.0
                         : crossSize - _getCrossSize(child);
      break;
    case CrossAxisAlignment.center:
      childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0;
      break;
    case CrossAxisAlignment.stretch:
      childCrossPosition = 0.0;
      break;
    case CrossAxisAlignment.baseline:
      childCrossPosition = 0.0;
      if (_direction == Axis.horizontal) {
        assert(textBaseline != null);
        final double distance = child.getDistanceToBaseline(textBaseline, onlyReal: true);
        if (distance != null)
          childCrossPosition = maxBaselineDistance - distance;
      }
      break;
  }
  if (flipMainAxis)
    childMainPosition -= _getMainSize(child);
  switch (_direction) {
    case Axis.horizontal:
      childParentData.offset = new Offset(childMainPosition, childCrossPosition);
      break;
    case Axis.vertical:
      childParentData.offset = new Offset(childCrossPosition, childMainPosition);
      break;
  }
  if (flipMainAxis) {
    childMainPosition -= betweenSpace;
  } else {
    childMainPosition += _getMainSize(child) + betweenSpace;
  }
  child = childParentData.nextSibling;
}

最后,則是根據交叉軸的對齊方式設置,對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
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容