Flutter實戰之基本佈局篇

JulyYu發表於2019-08-07

佈局

The core of Flutter’s layout mechanism is widgets. In Flutter, almost everything is a widget—even layout models are widgets.

1、Column & Row & Flex

將Column、Row、Flex放在一小節是因為不同之處只是對子Widget佈局方式不同。Column垂直佈局子元件,Row是水平佈局子元件,Flex根據direction設定Axis.horizontal或是Axis.vertical。

Flutter實戰之基本佈局篇 Flutter實戰之基本佈局篇
Column(
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
Row(
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
Flex(
  direction: Axis.vertical,
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
Flex(
  direction: Axis.horizontal,
  children: <Widget>[
    Text("text"),
    Text("text"),
    Text("text"),
  ],
);
複製程式碼

另外元件都不支援滑動,一旦子元件較多超出可顯示範圍,在Debug模式下會出現warning提示錯誤。通常情況下一般是使用ListView而非Column去顯示列表,當然可以在Column、Row、Flex外層巢狀SingleChildScrollView使其支援滑動。

Flutter實戰之基本佈局篇

Tips

  • Column和Row不支援對Padding,margin以及寬高的設定,一般外層巢狀Container或是Padding元件實現約束佈局寬度和間距。
  • Expanded元件也是在Column和Row中經常使用,通過它對子元件進行權重大小劃分。類似於Android的XML佈局檔案中weight屬性,Expanded中有flex引數用於設定比重比例,數值越大比重越大。
  • MainAxisAlignment屬性是與當前元件方向一致的軸,例如Row的主軸是水平方向,當設定MainAxisAlignment對於Row來說是在水平方向。支援的設定引數有:start、end、center、spaceBetween、spaceAround、spaceEvenly。詳見文件
  • CrossAxisAlignment屬性是與當前元件方向成垂直的軸,例如Row的交叉軸就是垂直方向,當設定MainAxisAlignment對於Row來說是在垂直方向。支援的設定引數有:start、end、center、stretch、baseline詳見文件
  • MainAxisSize屬性可設定:max、min,預設為max。類似於Android中math_content和wrap_content,當為min時大小根據子元件約束大小如果為max時大小根據自身約束大小。
  • Column和Row繼承自Flex。個人理解在一些特定UI佈局中可以使用Flex,例如需要切換垂直和水平佈局的時候選用。

2、Expanded & Flexible

這節是彈性佈局,上節已經有提到Column和Row使用子元件時通過巢狀彈性佈局Expanded重來分配佈局空間,同時也說過Flutter中彈性佈局類似於Android中XML的weight屬性。可知Expanded元件繼承自Flexible,唯獨不同的是Flexible的fit屬性值預設FlexFit.loose並且可設定,Expanded的fit屬性值是FlexFit.tight並且不能設定。

Flexible

class Flexible extends ParentDataWidget<Flex> {
  /// Creates a widget that controls how a child of a [Row], [Column], or [Flex]
  /// flexes.
  const Flexible({
    Key key,
    this.flex = 1,
    this.fit = FlexFit.loose,
    @required Widget child,
  }) : super(key: key, child: child);
複製程式碼

Expanded

const Expanded({
  Key key,
  int flex = 1,
  @required Widget child,
}) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
複製程式碼

FlexFit屬性:tight、loose。tight會讓子元件強制填充可用最大布局空間;loose會讓子元件儘可能填充可用佈局空間。字面上的意思tight比loose更強烈,如下例項設定tight比起loose會佔用更多佈局空間,而loose會根據巢狀的元件而定是否根據比重填充更多空間。

Column(
        children: <Widget>[
          Flexible(
            flex: 2,
            child: Container(
              color: Colors.blue,
              child: Center(
                child: Text(
                  "Flexible 2",
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          Flexible(
            flex: 1,
            fit: FlexFit.tight,
            child: Container(
              child: Text(
                "Flexible 1 FlexFit.tight",
              ),
            ),
          ),
          Flexible(
            flex: 1,
            fit: FlexFit.tight,
            child: Container(
              child: Text(
                "Flexible 1 FlexFit.tight",
              ),
            ),
          ),
          Center(
            child: Text(
              "NO Flexible",
            ),
          ),
        ],
      );
複製程式碼
Flutter實戰之基本佈局篇 Flutter實戰之基本佈局篇

Tips

  • 例如Flexible內部巢狀了Center元件即便FlexFit是loose元件填充空間還是會按照比重獲取對應比例的大小。而沒有內部沒有巢狀Center元件的Flexible則沒有按照比重填充空間而是根據自身元件實際大小填充。詳見文件
  • Spacer是一個空元件,主要用於填充空白區域,預設flex為1。
  • Flexible可以解決在Column和Row設定子元件Text的文字超出範圍警告問題。原先以為使用Text屬性overflow: TextOverflow.ellipsis可以解決但實際情況不可行。其實只需要在Text外層巢狀Flexible並能解決。

3、Stack

A widget that positions its children relative to the edges of its box.

Stack用於建立重疊樣式的佈局,子元件可以在Stack中疊加顯示。同時可以使用Positioned元件對子元件做定位操作,使子元件根據Positioned定義的位置引數顯示在Stack對應位置。或許會想到Stack和RelativeLayout是否有點相似,但注意的是Stack並沒有RelativeLayout的blow和above的方法讓元件之間有定位關係,它的定位關係只有子元件和Stack的關係。

Stack(
        fit: StackFit.expand,
        children: <Widget>[
            Text("1"),
            Text("2"),
            Text("3"),
            ],
     )
複製程式碼

新增Positioned讓子元件在Stack中定位顯示

Stack(
        fit: StackFit.expand,
        children: <Widget>[
            Positioned(
            top: 300,
            left: 40,
            child: Text("1"),
            ),
            Text("2"),
            Text("3"),
        ],
    )
複製程式碼
Flutter實戰之基本佈局篇 Flutter實戰之基本佈局篇

Tips

  • Stack的fit用於設定Stack空間大小,預設設定為loose,可選擇expand、passthrough。可以認為Stack預設大小是根據它未定位的子元件大小決定。若選擇expand則填充可用最大布局空間,passthrough則是根據父元件的約束而定。
  • Stack的子元件重疊若之後新增元件會覆蓋之前元件,例如後新增元件有背景色等則之前新增的元件會不可見。
  • Stack中未做定位處理的子元件預設起始位置在top-left。
  • 若子元件定位位置超出Stack的大小,則子元件會被切割顯示。

4、CustomSingleChildLayout

自定義單個元件佈局,可通過自定義delegate控制父級大小和子元件約束。delegate需要我們自行實現,通過繼承SingleChildLayoutDelegate實現我們需要的約束條件。

Container(
        color: Colors.blue,
        child: CustomSingleChildLayout(
            delegate: CustomSingleDelegate(),
            child: Center(
                child: Text("llll"),
            ),
        ),
    )
複製程式碼

這裡實現了一個簡單delegate。getPositionForChild方法可以獲取父級和子元件的大小設定偏移量;getSize方法獲取父級元件大小並重新設定約束;getConstraintsForChild獲取子元件約束並重設約束;

class CustomSingleDelegate extends SingleChildLayoutDelegate {
  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // return super.getPositionForChild(size, childSize);
    // 偏移量
    return Offset(0, 20);
  }
  @override
  Size getSize(BoxConstraints constraints) {
    //獲取到父級元件約束
    //    return super.getSize(constraints);
    return Size(200,200);
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    //獲取子元件約束
    //    return super.getConstraintsForChild(constraints);
    return constraints.enforce(BoxConstraints(maxHeight: 100,maxWidth: 100));
  }

  @override
  bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
    return false;
  }

  CustomSingleDelegate();
}
複製程式碼

如下圖所示為預設使用CustomSingleDelegate和修改CustomSingleDelegate各個方法之後的對比效果。可以看出通過CustomSingleDelegate可以設定子元件和父級元件各個約束引數達到預期想要的顯示效果。

Flutter實戰之基本佈局篇 Flutter實戰之基本佈局篇

Tips

  • CustomSingleChildLayout為開發者提供了一種自定義開發元件約束的能力,通過父級自身去控制自身約束大小而不像Stack依賴於未定位子元件的約束做為約束條件。
  • 同時CustomSingleChildLayout中子元件若在外層巢狀Center後它的約束好像就不跟隨父級,比如父級約束Size(200,200),設定子元件偏移量Offset(0, 300)即使超出父級約束還是能在佈局居中顯示。

5、CustomMultiChildLayout

CustomMultiChildLayout和Stack都繼承自MultiChildRenderObjectWidget。但可以說CustomMultiChildLayout是Stack的高階使用或者說是CustomSingleChildLayout的進階版,從控制一個子元件約束變為對多個子元件約束當然複雜度也就升級了。CustomMultiChildLayout使用MultiChildLayoutDelegate控制約束,children建立LayoutId的子元件並設定元件id。同樣的MultiChildLayoutDelegate需要自行實現。

Container(
    color: Colors.blue,
    child: CustomMultiChildLayout(
      delegate: CustomMultiChildDelegate(),
      children: <Widget>[
        LayoutId(
          id: CustomMultiChildDelegate.leftChild,
          child: Container(color: Colors.green,child: Text("left"),),
        ),
        LayoutId(
          id: CustomMultiChildDelegate.rightChild,
          child: Container(color: Colors.red,child: Text("right"),),
        )
      ],
    ),
  )
複製程式碼

自定義一個CustomMultiChildDelegate繼承MultiChildLayoutDelegate。在這裡進行父級大小設定和子元件約束設定。performLayout方法是佈局設定主入口,在這裡去設定layoutChild約束子元件佈局和positionChild定位子元件位置;getSize方法設定父級元件大小;layoutChild獲取到子元件約束設定;positionChild獲取子元件定位設定;

class CustomMultiChildDelegate extends MultiChildLayoutDelegate {
  static const String leftChild = "left";
  static const String rightChild = "right";

  @override
  Size layoutChild(Object childId, BoxConstraints constraints) {
    //獲取子元件約束
    return super.layoutChild(childId, constraints);
  }

  @override
  void positionChild(Object childId, Offset offset) {
    // 設定子元件偏移量
    super.positionChild(childId, offset);
  }

  @override
  Size getSize(BoxConstraints constraints) {
    //父級大小
    //return super.getSize(constraints);
    constraints = BoxConstraints(maxWidth: 100,maxHeight: 100);
    return Size(100, 100);
  }

  @override
  void performLayout(Size size) {
     //佈局設定入口,layoutChild和positionChild在此呼叫
    layoutChild(leftChild, BoxConstraints(minHeight: 200, minWidth: 200));
    layoutChild(rightChild, BoxConstraints(maxHeight: 60, maxWidth: 50));
    positionChild(leftChild, Offset(size.width + 30, 0));
    positionChild(leftChild, Offset(size.width - 30, 0));
    positionChild(rightChild, Offset(0, 0));
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
    return false;
  }

  CustomMultiChildDelegate();
}
複製程式碼

Flutter實戰之基本佈局篇
*CustomMultiChildLayout中當子元件約束超出父級大小的時候如何顯示。在之前CustomSingleChildLayout中有提到過子元件巢狀Center會導致子元件在父級約束大小外可正常顯示。下面就看看但CustomMultiChildLayout的子元件超出父級約束後,外部元件是否也可以正常顯示。

下面實驗使用Row作為根佈局,新增CustomMultiChildLayout讓rightChild超出父級,然後在Row水平位置再新增Text元件,檢視Text是否可以正常顯示以及預設顯示的位置。

//水平方向佈局中增加自定義約束佈局和另外的Text元件
Row(
    children: <Widget>[
      Container(
        color: Colors.blue,
        child: CustomMultiChildLayout(
          delegate: CustomMultiChildDelegate(),
          children: <Widget>[
            LayoutId(
              id: CustomMultiChildDelegate.leftChild,
              child: Container(color: Colors.green,child: Text("left"),),
            ),
            LayoutId(
              id: CustomMultiChildDelegate.rightChild,
              child: Container(color: Colors.red,child: Text("right" ),),
            )
          ],
        ),
      ),
      //
      Text("ooooo")
    ],
  )
複製程式碼

可以看到Text能夠正常顯示並且沒有被CustomMultiChildLayout的rightChild覆蓋,位置顯示則定位在CustomMultiChildLayout設定的大小之後。

Flutter實戰之基本佈局篇

Tips

  • CustomMultiChildLayout的子元件必須使用LayoutId巢狀以設定Id。
  • layoutChild和positionChild並不會主動呼叫,務必在performLayout方法中先呼叫layoutChild約束子元件然後呼叫positionChild定位子元件。
  • 為子元件設定最大BoxConstraints佈局約束時注意max和min的區別,當設定max未設定min時子元件大小是根據當前內容大小填充佈局,若設定min後子元件預設填充min設定值大小。
  • 同樣CustomMultiChildLayout的子元件約束不限制於父級,當子元件偏移量超出父級元件大小時也可以正常顯示。這點在介紹CustomSingleChildLayout時也已經提到過。

相關文章