【Flutter學習】頁面布局之其它布局處理


一,概述  

  Flutter中擁有30多種預定義的布局widget,常用的有ContainerPaddingCenterFlexRowColumListViewGridView。按照《Flutter技術入門與實戰》上面來說的話,大概分為四類

  • 基礎布局組件Container(容器布局),Center(居中布局),Padding(填充布局),Align(對齊布局),Colum(垂直布局),Row(水平布局),Expanded(配合Colum,Row使用),FittedBox(縮放布局),Stack(堆疊布局),overflowBox(溢出父視圖容器)。
  • 寬高尺寸處理SizedBox(設置具體尺寸),ConstrainedBox(限定最大最小寬高布局),LimitedBox(限定最大寬高布局),AspectRatio(調整寬高比),FractionallySizedBox(百分比布局)
  • 列表和表格處理ListView(列表),GridView(網格),Table(表格)
  • 其它布局處理:Transform(矩陣轉換),Baseline(基准線布局),Offstage(控制是否顯示組件),Wrap(按寬高自動換行布局)

二,其它布局處理

  • Transform(矩陣轉換)
    • 介紹
      Transform在介紹Container的時候有提到過,就是做矩陣變換的。Container中矩陣變換就是使用的Transform。
    • 布局行為
      有過其他平台經驗的,對Transform應該不會陌生。可以對child做平移、旋轉、縮放等操作。
    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Transform
    • 構造函數
      const Transform({
        Key key,
        @required this.transform,
        this.origin,
        this.alignment,
        this.transformHitTests = true,
        Widget child,
      })

      上面是其默認的構造函數,Transform也提供下面三種構造函數:

      Transform.rotate
      Transform.translate
      Transform.scale
    • 參數含義
      • transform:一個4x4的矩陣。不難發現,其他平台的變換矩陣也都是四階的。一些復合操作,僅靠三維是不夠的,必須采用額外的一維來補充,感興趣的同學可以自行搜索了解。

         

      • origin:旋轉點,相對於左上角頂點的偏移。默認旋轉點事左上角頂點。

         

      • alignment:對齊方式。

         

      • transformHitTests:點擊區域是否也做相應的改變。

          
  • Baseline(基准線布局)
    • 介紹

      Baseline這個控件,做過移動端開發的都會了解過,一般文字排版的時候,可能會用到它。它的作用很簡單,根據child的baseline,來調整child的位置。例如兩個字號不一樣的文字,希望底部在一條水平線上,就可以使用這個控件,是一個非常基礎的控件。

      關於字符的Baseline,可以看下下面這張圖,這具體就涉及到了字體排版,感興趣的同學可以自行了解。

    • 布局行為

      Baseline控件布局行為分為兩種情況:

      • 如果child有baseline,則根據child的baseline屬性,調整child的位置;
      • 如果child沒有baseline,則根據child的bottom,來調整child的位置。
    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Baseline
    • 構造函數
      const Baseline({
        Key key,
        @required this.baseline,
        @required this.baselineType,
        Widget child
      })
    • 參數含義 

      baseline:baseline數值,必須要有,從頂部算。

      baselineType:bseline類型,也是必須要有的,目前有兩種類型:

      • alphabetic:對齊字符底部的水平線;
      • ideographic:對齊表意字符的水平線。
  • Offstage(控制是否顯示組件)
    • 介紹
      Offstage的作用很簡單,通過一個參數,來控制child是否顯示,日常使用中也算是比較常用的控件。
    • 布局行為

      Offstage的布局行為完全取決於其offstage參數

      • 當offstage為true,當前控件不會被繪制在屏幕上,不會響應點擊事件,也不會占用空間;
      • 當offstage為false,當前控件則跟平常用的控件一樣渲染繪制;

      另外,當Offstage不可見的時候,如果child有動畫,應該手動停掉,Offstage並不會停掉動畫。

    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Offstage
    • 構造函數
      const Offstage(
      {
        Key key,
      this.offstage = true,
      Widget child
      }
      )
    • 參數含義
      offstage:默認為true,也就是不顯示,當為flase的時候,會顯示該控件。 
  • Wrap(按寬高自動換行布局)
    • 介紹
      其實Wrap實現的效果,Flow可以很輕松,而且可以更加靈活的實現出來。
    • 布局行為

      Flow可以很輕易的實現Wrap的效果,但是Wrap更多的是在使用了Flex中的一些概念,某種意義上說是跟Row、Column更加相似的。

      單行的Wrap跟Row表現幾乎一致,單列的Wrap則跟Row表現幾乎一致。但Row與Column都是單行單列的,Wrap則突破了這個限制,mainAxis上空間不足時,則向crossAxis上去擴展顯示。

      從效率上講,Flow肯定會比Wrap高,但是Wrap使用起來會方便一些。

    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Wrap
    • 構造函數
      Wrap({
        Key key,
        this.direction = Axis.horizontal,
        this.alignment = WrapAlignment.start,
        this.spacing = 0.0,
        this.runAlignment = WrapAlignment.start,
        this.runSpacing = 0.0,
        this.crossAxisAlignment = WrapCrossAlignment.start,
        this.textDirection,
        this.verticalDirection = VerticalDirection.down,
        List<Widget> children = const <Widget>[],
      })
    • 參數含義 
      • direction:主軸(mainAxis)的方向,默認為水平。

         

      • alignment:主軸方向上的對齊方式,默認為start。

         

      • spacing:主軸方向上的間距。

         

      • runAlignment:run的對齊方式。run可以理解為新的行或者列,如果是水平方向布局的話,run可以理解為新的一行。

         

      • runSpacing:run的間距。

         

      • crossAxisAlignment:交叉軸(crossAxis)方向上的對齊方式。

         

      • textDirection:文本方向。

         

      • verticalDirection:定義了children擺放順序,默認是down,見Flex相關屬性介紹。

三,使用實例

  • Transform(矩陣轉換)
    Center(
      child: Transform(
        transform: Matrix4.rotationZ(0.3),
        child: Container(
          color: Colors.blue,
          width: 100.0,
          height: 100.0,
        ),
      ),
    )

    將Container繞z軸旋轉了

    效果圖:


    源碼解析

    我們來看看它的繪制代碼:

    if (child != null) {
      final Matrix4 transform = _effectiveTransform;
      final Offset childOffset = MatrixUtils.getAsTranslation(transform);
    if (childOffset == null)
      context.pushTransform(needsCompositing, offset, transform, super.paint);
    else
      super.paint(context, offset + childOffset);
    }

    整個繪制代碼不復雜,如果child有偏移的話,則將兩個偏移相加,進行繪制。如果child沒有偏移的話,則按照設置的offset、transform進行繪制。

    使用場景:這個控件算是較常見的控件,很多平移、旋轉、縮放都可以使用的到。如果只是單純的進行變換的話,用Transform比用Container效率會更高。

  • Baseline(基准線布局)
    new Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        new Baseline(
          baseline: 50.0,
          baselineType: TextBaseline.alphabetic,
          child: new Text(
            'TjTjTj',
            style: new TextStyle(
              fontSize: 20.0,
              textBaseline: TextBaseline.alphabetic,
            ),
          ),
        ),
        new Baseline(
          baseline: 50.0,
          baselineType: TextBaseline.alphabetic,
          child: new Container(
            width: 30.0,
            height: 30.0,
            color: Colors.red,
          ),
        ),
        new Baseline(
          baseline: 50.0,
          baselineType: TextBaseline.alphabetic,
          child: new Text(
            'RyRyRy',
            style: new TextStyle(
              fontSize: 35.0,
              textBaseline: TextBaseline.alphabetic,
            ),
          ),
        ),
      ],
    )

    效果圖:

    源碼解析:

    我們來看看源碼中具體計算尺寸的這段代碼

    child.layout(constraints.loosen(), parentUsesSize: true);
    final double childBaseline = child.getDistanceToBaseline(baselineType);
    final double actualBaseline = baseline;
    final double top = actualBaseline - childBaseline;
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = new Offset(0.0, top);
    final Size childSize = child.size;
    size = constraints.constrain(new Size(childSize.width, top + childSize.height));

    getDistanceToBaseline這個函數是獲取baseline數值的,存在的話,就取這個值,不存在的話,則取其高度。

    整體的計算過程
    (1)獲取child的 baseline 值;
    (2)計算出top值,其為 baseline - childBaseline,這個值有可能為負數;
    (3)計算出Baseline控件尺寸,寬度為child的,高度則為 top + childSize.height。

  • Offstage(控制是否顯示組件)
    Column(
      children: <Widget>[
        new Offstage(
          offstage: offstage,
          child: Container(color: Colors.blue, height: 100.0),
        ),
        new CupertinoButton(
          child: Text("點擊切換顯示"),
          onPressed: () {
            setState(() {
              offstage = !offstage;
            });
          },
        ),
      ],
    )

    源碼解析

    我們先來看下Offstage的computeIntrinsicSize相關的方法:

    @override
     double computeMinIntrinsicWidth(double height) {
     if (offstage)
       return 0.0;
       return super.computeMinIntrinsicWidth(height);
     }

    可以看到,當offstage為true的時候,自身的最小以及最大寬高都會被置為0.0。

    接下來我們來看下其hitTest方法:

    @override
     bool hitTest(HitTestResult result, { Offset position }) {
       return !offstage && super.hitTest(result, position: position);
    }

    當offstage為true的時候,也不會去執行。

    最后我們來看下其paint方法:

    @override
    void paint(PaintingContext context, Offset offset) {
     if (offstage)
      return;
      super.paint(context, offset);
     }

    當offstage為true的時候直接返回,不繪制了。

    到此,跟上面所說的布局行為對應上了。我們一定要清楚一件事情,Offstage並不是通過插入或者刪除自己在widget tree中的節點,來達到顯示以及隱藏的效果,而是通過設置自身尺寸、不響應hitTest以及不繪制,來達到展示與隱藏的效果。

  • Wrap(按寬高自動換行布局)
    Wrap(
      spacing: 8.0, // gap between adjacent chips
      runSpacing: 4.0, // gap between lines
      children: <Widget>[
        Chip(
          avatar: CircleAvatar(
              backgroundColor: Colors.blue.shade900, child: new Text('AH', style: TextStyle(fontSize: 10.0),)),
          label: Text('Hamilton'),
        ),
        Chip(
          avatar: CircleAvatar(
              backgroundColor: Colors.blue.shade900, child: new Text('ML', style: TextStyle(fontSize: 10.0),)),
          label: Text('Lafayette'),
        ),
        Chip(
          avatar: CircleAvatar(
              backgroundColor: Colors.blue.shade900, child: new Text('HM', style: TextStyle(fontSize: 10.0),)),
          label: Text('Mulligan'),
        ),
        Chip(
          avatar: CircleAvatar(
              backgroundColor: Colors.blue.shade900, child: new Text('JL', style: TextStyle(fontSize: 10.0),)),
          label: Text('Laurens'),
        ),
      ],
    )

    效果圖:


    源碼解析

    我們來看下其布局代碼。

    第一步,如果第一個child為null,則將其設置為最小尺寸。

    RenderBox child = firstChild;
    if (child == null) {
      size = constraints.smallest;
      return;
    }

    第二步,根據direction、textDirection以及verticalDirection屬性,計算出相關的mainAxis、crossAxis是否需要調整方向,以及主軸方向上的限制。

    double mainAxisLimit = 0.0;
    bool flipMainAxis = false;
    bool flipCrossAxis = false;
    switch (direction) {
      case Axis.horizontal:
        childConstraints = new BoxConstraints(maxWidth: constraints.maxWidth);
        mainAxisLimit = constraints.maxWidth;
        if (textDirection == TextDirection.rtl)
           flipMainAxis = true;
        if (verticalDirection == VerticalDirection.up)
           flipCrossAxis = true;
           break;
      case Axis.vertical:
          childConstraints = new BoxConstraints(maxHeight: constraints.maxHeight);
          mainAxisLimit = constraints.maxHeight;
        if (verticalDirection == VerticalDirection.up)
          flipMainAxis = true;
        if (textDirection == TextDirection.rtl)
          flipCrossAxis = true;
       break;
    }

    第三步,計算出主軸以及交叉軸的區域大小。

    while (child != null) {
    child.layout(childConstraints, parentUsesSize: true);
    final double childMainAxisExtent = _getMainAxisExtent(child);
    final double childCrossAxisExtent = _getCrossAxisExtent(child);
    if (childCount > 0 && runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit) {
      mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
      crossAxisExtent += runCrossAxisExtent;
    if (runMetrics.isNotEmpty)
      crossAxisExtent += runSpacing;
      runMetrics.add(new _RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount));
      runMainAxisExtent = 0.0;
      runCrossAxisExtent = 0.0;
      childCount = 0;
    }
    runMainAxisExtent += childMainAxisExtent;
    if (childCount > 0)
      runMainAxisExtent += spacing;
      runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent);
      childCount += 1;
      final WrapParentData childParentData = child.parentData;
      childParentData._runIndex = runMetrics.length;
      child = childParentData.nextSibling;
    }

    第四步,根據direction設置Wrap的尺寸。

    switch (direction) {
     case Axis.horizontal:
      size = constraints.constrain(new Size(mainAxisExtent, crossAxisExtent));
      containerMainAxisExtent = size.width;
      containerCrossAxisExtent = size.height;
      break;
     case Axis.vertical:
      size = constraints.constrain(new Size(crossAxisExtent, mainAxisExtent));
      containerMainAxisExtent = size.height;
      containerCrossAxisExtent = size.width;
      break;
    }

    第五步,根據runAlignment計算出每一個run之間的距離,幾種屬性的差異,之前文章介紹過,在此就不做詳細闡述。

    final double crossAxisFreeSpace = math.max(0.0, containerCrossAxisExtent - crossAxisExtent);
    double runLeadingSpace = 0.0;
    double runBetweenSpace = 0.0;
    switch (runAlignment) {
       case WrapAlignment.start:
      break;
       case WrapAlignment.end:
         runLeadingSpace = crossAxisFreeSpace;
      break;
       case WrapAlignment.center:
         runLeadingSpace = crossAxisFreeSpace / 2.0;
      break;
       case WrapAlignment.spaceBetween:
         runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0;
      break;
       case WrapAlignment.spaceAround:
         runBetweenSpace = crossAxisFreeSpace / runCount;
         runLeadingSpace = runBetweenSpace / 2.0;
      break;
       case WrapAlignment.spaceEvenly:
         runBetweenSpace = crossAxisFreeSpace / (runCount + 1);
         runLeadingSpace = runBetweenSpace;
       break;
    }

    第六步,根據alignment計算出每一個run中child的主軸方向上的間距。

    switch (alignment) {
      case WrapAlignment.start:
        break;
      case WrapAlignment.end:
         childLeadingSpace = mainAxisFreeSpace;
        break;
      case WrapAlignment.center:
         childLeadingSpace = mainAxisFreeSpace / 2.0;
        break;
      case WrapAlignment.spaceBetween:
         childBetweenSpace = childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0;
        break;
      case WrapAlignment.spaceAround:
         childBetweenSpace = mainAxisFreeSpace / childCount;
         childLeadingSpace = childBetweenSpace / 2.0;
        break;
      case WrapAlignment.spaceEvenly:
         childBetweenSpace = mainAxisFreeSpace / (childCount + 1);
         childLeadingSpace = childBetweenSpace;
        break;
    }

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

    while (child != null) {
    final WrapParentData childParentData = child.parentData;
    if (childParentData._runIndex != i)
     break;
      final double childMainAxisExtent = _getMainAxisExtent(child);
      final double childCrossAxisExtent = _getCrossAxisExtent(child);
      final double childCrossAxisOffset = _getChildCrossAxisOffset(flipCrossAxis, runCrossAxisExtent, childCrossAxisExtent);
    if (flipMainAxis)
      childMainPosition -= childMainAxisExtent;
      childParentData.offset = _getOffset(childMainPosition, crossAxisOffset + childCrossAxisOffset);
    if (flipMainAxis)
      childMainPosition -= childBetweenSpace;
    else
      childMainPosition += childMainAxisExtent + childBetweenSpace;
      child = childParentData.nextSibling;
    }
    
    if (flipCrossAxis)
      crossAxisOffset -= runBetweenSpace;
    else
      crossAxisOffset += runCrossAxisExtent + runBetweenSpace;

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

    如果第一個child為null,則將Wrap設置為最小尺寸,布局結束;
    根據direction、textDirection以及verticalDirection屬性,計算出mainAxis、crossAxis是否需要調整方向;
    計算出主軸以及交叉軸的區域大小;
    根據direction設置Wrap的尺寸;
    根據runAlignment計算出每一個run之間的距離;
    根據alignment計算出每一個run中child的主軸方向上的間距
    調整每一個child的位置。

四,參考  

Flutter學習之認知基礎組件
Flutter布局

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM