Flutter佈局詳解,必知必會

前言

本文的目的是爲了讓讀者掌握不一樣佈局類Widget的佈局特色,分享一些在實際使用過程遇到的一些問題,在《Flutter實戰》這本書中已經講解的很詳細了,本文主要是對其內容的濃縮及實際遇到的問題的補充。前端

什麼是佈局類Widget

佈局類Widget就是指直接或間接繼承(包含)MultiChildRenderObjectWidget的Widget,它們通常都會有一個children屬性用於接收子Widget。在Flutter中Element樹纔是最終的繪製樹,Element樹是經過widget樹來建立的(經過Widget.createElement()),widget其實就是Element的配置數據。它的最終佈局、UI界面渲染都是經過RenderObject對象來實現的,這裏的細節我就不詳細描述了,由於我也不懂。不過感興趣的小夥伴也能夠看看本專欄的Flutter視圖的Layout與Paint這篇文章。git

Flutter中主要有如下幾種佈局類的Widget:github

  • 線性佈局Row和Column
  • 彈性佈局Flex
  • 流式佈局Wrap、Flow
  • 層疊佈局Stack、Positioned

本文[Demo地址](https://github.com/xqqlv/flutte_layout_demo) bash

線性佈局Row和Column

線性佈局實際上是指沿水平或垂直方向排布子Widget,Flutter中經過Row來實現水平方向的子Widegt佈局,經過Column來實現垂直方向的子Widget佈局。他們都繼承Flex,因此它們有不少類似的屬性。 markdown

Row
Column

在前端的Flex佈局中,默認存在兩根軸:水平的主軸(main axis)和垂直的交叉軸(cross axis)。主軸的開始位置(與邊框的交叉點)叫作main start,結束位置叫作main end;交叉軸的開始位置叫作cross start,結束位置叫作cross end。與Flutter中MainAxisAlignment和CrossAxisAlignment相似,分別表明主軸對齊和縱軸對齊。less

源碼屬性解讀

Row({
    .....
    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>[],
  })

  Column({
    .....
    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>[],
  }) 
複製代碼
  • textDirection:表示水平方向子widget的佈局順序(是從左往右仍是從右往左),默認爲系統當前Locale環境的文本方向(如中文、英語都是從左往右,而阿拉伯語是從右往左)。
  • 主軸方向: Row即爲水平方向,Column爲垂直方向
  • mainAxisAlignment 主軸方向,對child起做用
    • center:將children放置在主軸的中心
    • start:將children放置在主軸的起點
    • end:將children放置在主軸的末尾
    • spaceAround:將主軸方向上的空白區域均分,使children之間的空白區域相等,可是首尾child的靠邊間距爲空白區域爲1/2
    • spaceBetween:將主軸方向上的空白區域均分,使children之間的空白區域相等,首尾child靠邊沒有間隙
    • spaceEvenly:將主軸方向上的空白區域均分,使得children之間的空白區域相等,包括首尾child
  • mainAxisSize max表示儘量佔多的控件,min會致使控件聚攏在一塊兒
  • crossAxisAlignment 交叉軸方向,對child起做用
    • baseline:使children baseline對齊
    • center:children在交叉軸上居中展現
    • end:children在交叉軸上末尾展現
    • start:children在交叉軸上起點處展現
    • stretch:讓children填滿交叉軸方向
  • verticalDirection ,child的放置順序
    • VerticalDirection.down,在Row中就是從左邊到右邊,Column表明從頂部到底部
    • VerticalDirection.up,相反

Row

示例代碼
ListView(
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Text("我是Row的子控件 "),
            Text("MainAxisAlignment.start")
          ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("我是Row的子控件 "),
            Text("MainAxisAlignment.center")
          ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            Text("我是Row的子控件 "),
            Text("MainAxisAlignment.end")
          ],
        ),
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          verticalDirection: VerticalDirection.up,
          children: <Widget>[
            Text(" Hello World ", style: TextStyle(fontSize: 30.0),),
            Text(" I am Jack "),
          ],
      ],
    )
複製代碼
代碼運行效果

image.png
前3個Row很簡單,只是設置了主軸方向的對齊方式;第四個Row測試的是縱軸的對齊方式,因爲兩個子Text字體不同,因此其高度也不一樣,咱們指定了verticalDirection值爲VerticalDirection.up,即從低向頂排列,而此時crossAxisAlignment值爲CrossAxisAlignment.start表示底對齊。你們能夠參考上面Row和Column的主側軸的示意圖,看看佈局是否是正確的,還有不少種狀況就不一一列舉了。

Column

示例代碼
ListView(children: <Widget>[
      Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text("我是Colum的子控件"),
          Text("CrossAxisAlignment.start"),
        ],
      ),
      Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Text("我是Colum的子控件"),
          Text("CrossAxisAlignment.center"),
        ],
      ),
      Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          Text("我是Colum的子控件"),
          Text("CrossAxisAlignment.end"),
        ],
      ),
    ],)
複製代碼
代碼運行效果

image.png
Column和Row差很少,只是佈局方向不同而已,你們能夠參考着看,這裏就再也不贅述了。

實際使用

因爲篇幅有限,我就不詳細講解實際遇到的問題了,只說現象和解決辦法:ide

  • 若是Row裏面嵌套Row,或者Column裏面再嵌套Column,那麼只有對最外面的Row或Column會佔用盡量大的空間,裏面Row或Column所佔用的空間爲實際大小,若是要讓裏面的Colum或Row佔滿外部Colum或Row,可使用Expanded widget
  • 若是使用Column發現超範圍,可用SingleChildScrollView包裹,scrollDirection屬性設置滑動方向
  • 使用Column嵌套ListView/GridView的時候,會報異常信息【Viewports expand in the scrolling direction to fill their container...】,這種狀況flutter已給出解決辦法,將ListView/GridView的 shrinkWrap屬性設爲true
  • 有的時候修改Row/Column的verticalDirection會獲得很好的效果,好比須要頁面在底部須要幾個按鍵,也能夠用Stack來佈局,可是相對麻煩,並且有時還須要知道控件的大小,沒有verticalDirection方便

彈性佈局

彈性佈局是一種容許子widget按照必定比例來分配父容器空間的佈局方式,若是你知道了它的主軸方向,那就能夠用Row或Column了,通常狀況下,能夠用Flex的地方均可以用Row或者Column一塊兒使用,一般配合Expanded Widget來使用,一樣Expanded也不能脫離Flex單首創建。oop

Expanded

Expanded繼承自Flexible,Flexible是一個控制Row、Column、Flex等子組件如何佈局的組件,它能夠按比例「擴伸」Row、Column和Flex子widget所佔用的空間。佈局

const Expanded({
  int flex = 1, 
  @required Widget child,
})
複製代碼

flex爲彈性係數,若是爲0或null,則child是沒有彈性的,即不會被擴伸佔用的空間。若是大於0,全部的Expanded按照其flex的比例來分割主軸的所有空閒空間。post

示例代碼
Row(children: <Widget>[
        RaisedButton(
          onPressed: () {
            print('點擊紅色按鈕事件');
          },
          color: Colors.red,
          child: Text('紅色按鈕'),
        ),
        Expanded(
          flex: 1,
          child: RaisedButton(
            onPressed: () {
              print('點擊黃色按鈕事件');
            },
            color: Colors.yellow,
            child: Text('黃色按鈕'),
          ),
        ),
        RaisedButton(
          onPressed: () {
            print('點擊粉色按鈕事件');
          },
          color: Colors.green,
          child: Text('綠色按鈕'),
        ),
      ])
複製代碼
代碼運行效果

image.png

Flexible和 Expanded的區別
  • Flexible組件必須是Row、Column、Flex等組件的後裔,而且從Flexible到它封裝的Row、Column、Flex的路徑必須只包括StatelessWidgets或StatefulWidgets組件(不能是其餘類型的組件,像RenderObjectWidgets)
  • Row、Column、Flex會被Expanded撐開,充滿主軸可用空間,而Flexible不強制子組件填充可用空間,這是由於fit屬性的值不一樣,該屬性在Expanded中爲FlexFit.tight,Flexible爲FlexFit.loose,區別在於tight表示強制使子控件填充剩餘可用空間,loose表示最多填滿其在父控件所設置的比例,因此loose默認即爲控件的大小

流式佈局

流式佈局(Liquid)的特色(也叫"Fluid") 是頁面元素的寬度按照屏幕分辨率進行適配調整,但總體佈局不變。柵欄系統(網格系統),用戶標籤等。在Flutter中主要有Wrap和Flow兩種Widget實現。

Wrap

在介紹Row和Colum時,若是子widget超出屏幕範圍,則會報溢出錯誤,在Flutter中經過Wrap和Flow來支持流式佈局,溢出部分則會自動折行。

源碼屬性解讀
Wrap({
  ...
  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>[],
})
複製代碼

上述有不少屬性和Row的相同,其意義其實也是相同的,這裏我就不一一介紹了,主要介紹下不一樣的屬性:

  • spacing:主軸方向子widget的間距
  • runSpacing:縱軸方向的間距
  • runAlignment:縱軸方向的對齊方式
示例代碼
Wrap(
   spacing: 10.0,
   direction: Axis.horizontal,
   alignment: WrapAlignment.start,
   children: <Widget>[
     _card('關注'),
     _card('推薦'),
     _card('新時代'),
     _card('小視頻'),
     _card('黨媒推薦'),
     _card('中國新唱將'),
     _card('歷史'),
     _card('視頻'),
     _card('遊戲'),
     _card('頭條號'),
     _card('數碼'),
   ],
 )

  Widget _card(String title) {
    return Card(child: Text(title),);
  }
}
複製代碼
運行效果

image.png

小結
  • 使用Wrap能夠很輕鬆的實現流式佈局效果
  • Wrap支持設置流式佈局是縱向顯示或者是橫向顯示
  • 可使用alignment屬性來控制widgets的佈局方式

Flow

咱們通常不多會使用Flow,由於其過於複雜,須要本身實現子widget的位置轉換,在不少場景下首先要考慮的是Wrap是否知足需求。Flow主要用於一些須要自定義佈局策略或性能要求較高(如動畫中)的場景。Flow有以下優勢:

  • 性能好;Flow是一個對child尺寸以及位置調整很是高效的控件,Flow用轉換矩陣(transformation matrices)在對child進行位置調整的時候進行了優化:在Flow定位事後,若是child的尺寸或者位置發生了變化,在FlowDelegate中的paintChildren()方法中調用context.paintChild 進行重繪,而context.paintChild在重繪時使用了轉換矩陣(transformation matrices),並無實際調整Widget位置。
  • 靈活;因爲咱們須要本身實現FlowDelegate的paintChildren()方法,因此咱們須要本身計算每個widget的位置,所以,能夠自定義佈局策略。 缺點:
  • 使用複雜.
  • 不能自適應子widget大小,必須經過指定父容器大小或實現TestFlowDelegate的getSize返回固定大小。
示例代碼

咱們對六個色塊進行自定義流式佈局:

Flow(
  delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
  children: <Widget>[
    new Container(width: 80.0, height:80.0, color: Colors.red,),
    new Container(width: 80.0, height:80.0, color: Colors.green,),
    new Container(width: 80.0, height:80.0, color: Colors.blue,),
    new Container(width: 80.0, height:80.0,  color: Colors.yellow,),
    new Container(width: 80.0, height:80.0, color: Colors.brown,),
    new Container(width: 80.0, height:80.0,  color: Colors.purple,),
  ],
)
複製代碼

實現TestFlowDelegate:

class TestFlowDelegate extends FlowDelegate {
  EdgeInsets margin = EdgeInsets.zero;
  TestFlowDelegate({this.margin});
  @override
  void paintChildren(FlowPaintingContext context) {
    var x = margin.left;
    var y = margin.top;
    //計算每個子widget的位置 
    for (int i = 0; i < context.childCount; i++) {
      var w = context.getChildSize(i).width + x + margin.right;
      if (w < context.size.width) {
        context.paintChild(i,
            transform: new Matrix4.translationValues(
                x, y, 0.0));
        x = w + margin.left;
      } else {
        x = margin.left;
        y += context.getChildSize(i).height + margin.top + margin.bottom;
        //繪製子widget(有優化) 
        context.paintChild(i,
            transform: new Matrix4.translationValues(
                x, y, 0.0));
        x += context.getChildSize(i).width + margin.left + margin.right;
      }
    }
  }

  getSize(BoxConstraints constraints){
    //指定Flow的大小 
    return Size(double.infinity,200.0);
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }
}
複製代碼
運行效果

image.png
能夠看到咱們主要的任務就是實現paintChildren,它的主要任務是肯定每一個子widget位置。因爲Flow不能自適應子widget的大小,咱們經過在getSize返回一個固定大小來指定Flow的大小,實現起來仍是比較麻煩的。

小結
  • 參數簡單,不過須要本身定義delegate
  • delegate通常是爲了實現child的繪製,就是位置的擺放,不一樣狀況須要定義不一樣的delegate
  • 不一樣的delegate通常會提供實現的幾個方法:
    • getConstraintsForChild: 設置每一個child的佈局約束條件,會覆蓋已有的方式
    • getSize:設置控件的尺寸
    • shouldRelayout:表示是否須要從新佈局
  • 儘量的用Wrap,畢竟簡單

層疊佈局

層疊佈局和Web中的絕對定位、Android中的Frame佈局是類似的,子widget能夠根據到父容器四個角的位置來肯定自己的位置。絕對定位容許子widget堆疊(按照代碼中聲明的順序)。Flutter中使用Stack和Positioned來實現絕對定位,Stack容許子widget堆疊,而Positioned能夠給子widget定位(根據Stack的四個角)。

Stack

Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})
複製代碼
  • alignment:此參數決定如何去對齊沒有定位(沒有使用Positioned)或部分定位的子widget。所謂部分定位,在這裏特指沒有在某一個軸上定位:left、right爲橫軸,top、bottom爲縱軸,只要包含某個軸上的一個定位屬性就算在該軸上有定位。
  • textDirection:和Row、Wrap的textDirection功能同樣,都用於決定alignment對齊的參考系即:textDirection的值爲TextDirection.ltr,則alignment的start表明左,end表明右;textDirection的值爲TextDirection.rtl,則alignment的start表明右,end表明左。
  • fit:此參數用於決定沒有定位的子widget如何去適應Stack的大小。StackFit.loose表示使用子widget的大小,StackFit.expand表示擴伸到Stack的大小。
  • overflow:此屬性決定如何顯示超出Stack顯示空間的子widget,值爲Overflow.clip時,超出部分會被剪裁(隱藏),值爲Overflow.visible 時則不會。
下面是我用Stack實現的一個簡易的loading
class Loading extends StatelessWidget {
  /// ProgressIndicator的padding,決定loading的大小
  final EdgeInsets padding = EdgeInsets.all(30.0);

  /// 文字頂部距菊花的底部的距離
  final double margin = 10.0;

  /// 圓角
  final double cornerRadius = 10.0;

  final Widget _child;
  final bool _isLoading;
  final double opacity;
  final Color color;
  final String text;

  Loading({
    Key key,
    @required child,
    @required isLoading,
    this.text,
    this.opacity = 0.3,
    this.color = Colors.grey,
  })  : assert(child != null),
        assert(isLoading != null),
        _child = child,
        _isLoading = isLoading,
        super(key: key);

  @override
  Widget build(BuildContext context) {
    List<Widget> widgetList = List<Widget>();
    widgetList.add(_child);
    if (_isLoading) {
      final loading = [
        Opacity(
          opacity: opacity,
          child: ModalBarrier(dismissible: false, color: color),
        ),
        _buildProgressIndicator()
      ];
      widgetList.addAll(loading);
    }
    return Stack(
      children: widgetList,
    );
  }

  Widget _buildProgressIndicator() {
    return Center(
      child: Container(
        padding: padding,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            CupertinoActivityIndicator(),
            Padding(
                padding: EdgeInsets.only(top: margin),
                child: Text(text ?? '加載中...')),
          ],
        ),
        decoration: BoxDecoration(
            borderRadius: BorderRadius.all(Radius.circular(cornerRadius)),
            color: Colors.white),
      ),
    );
  }
}
複製代碼
顯示效果

image.png

本控件使用Stack封裝,你傳入的主視圖在最下面一層,背景層在中間,最上面一層爲菊花和文字loading,用isLoading控制顯示

Positioned

const Positioned({
  Key key,
  this.left, 
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  @required Widget child,
})
複製代碼

left、top 、right、 bottom分別表明離Stack左、上、右、底四邊的距離。width和height用於指定定位元素的寬度和高度,注意,此處的width、height 和其它地方的意義稍微有點區別,此處用於配合left、top 、right、 bottom來定位widget,舉個例子,在水平方向時,你只能指定left、right、width三個屬性中的兩個,如指定left和width後,right會自動算出(left+width),若是同時指定三個屬性則會報錯,垂直方向同理。

示例代碼
//經過ConstrainedBox來確保Stack佔滿屏幕
ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: Stack(
    alignment:Alignment.center , //指定未定位或部分定位widget的對齊方式
    children: <Widget>[
      Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18.0,
        child: Text("I am Jack"),
      ),
      Positioned(
        top: 18.0,
        child: Text("Your friend"),
      )        
    ],
  ),
);
複製代碼
運行效果:

image.png
因爲第一個子widget Text("Hello world")沒有指定定位,而且alignment值爲Alignment.center,因此,它會居中顯示。第二個子widget Text("I am Jack")只指定了水平方向的定位(left),因此屬於部分定位,即垂直方向上沒有定位,那麼它在垂直方向對齊方式則會按照alignment指定的對齊方式對齊,即垂直方向居中。對於第三個子widget Text("Your friend"),和第二個Text原理同樣,只不過是水平方向沒有定位,則水平方向居中。

本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。@xqqlv

相關文章
相關標籤/搜索