Flutter之佈局類Widget

Flutter

  • 原文博客地址: Flutter之佈局類Widget
  • 相關博客系列文章: Flutter和Dart系列文章
  • 相關Demo地址: GitHub地址
  • 佈局類Widget都會包含一個或多個子widget,不一樣的佈局類Widget對子widget排版(layout)方式不一樣
  • 上一篇文章中提到: Widget實際上就是Element的配置數據, Widget的功能是描述一個UI元素的一個配置數據, 而真正的UI渲染是由Element構成
  • Flutter中,根據Widget是否須要包含子節點將Widget分爲了三類,分別對應三種Element,以下表
Widget 對應的Element 用途
LeafRenderObjectWidget LeafRenderObjectElement Widget樹的葉子節點,用於沒有子節點的widget,一般基礎widget都屬於這一類,如TextImage
SingleChildRenderObjectWidget SingleChildRenderObjectElement 包含一個子Widget,如:ConstrainedBoxDecoratedBox
MultiChildRenderObjectWidget MultiChildRenderObjectElement 包含多個子Widget,通常都有一個children參數,接受一個Widget數組。如RowColumnStack

佈局類Widget

  • 佈局類Widget就是指直接或間接繼承(包含)MultiChildRenderObjectWidgetWidget,它們通常都會有一個children屬性用於接收子Widget
  • Widget的繼承關係以下:
    • Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget
  • RenderObjectWidget類中定義了建立、更新RenderObject的方法,子類必須實現他們
  • 對於佈局類Widget來講,其佈局算法都是經過對應的RenderObject對象來實現的
  • Flutter中主要有如下幾種佈局類的Widget
    • 線性佈局RowColumn
    • 彈性佈局Flex
    • 流式佈局WrapFlow
    • 層疊佈局StackPositioned

線性佈局

  • RowColumn是一種現行佈局的Widget, 都繼承自Flex
  • 所謂線性佈局,即指沿水平或垂直方向排布子Widget
  • 對於線性佈局,有主軸和縱軸之分,若是佈局是沿水平方,那麼主軸就指是水平方向,而縱軸即垂直方向;若是佈局沿垂直方向,那麼主軸就是指垂直方向,而縱軸就是水平方向
  • Row的主軸即爲水平方向, Column的主軸是垂直方向, 切二者的屬性和使用都同樣
  • 相關下定義的源碼以下:
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>[],
})

Column({
    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>[],
})  
複製代碼

相關屬性以下

mainAxisAlignment

Widget在主軸方向的排列方式, 爲方便如下皆稱Widget爲組件html

// 默認值
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start
複製代碼
  • start: 子widgets向主軸起點對其, 依次排列
  • end: 子widgets向主軸終點對其, 依次排列
  • center: 全部子widgets居中排列
  • spaceBetween: 均勻分配,相鄰widgets間距離相同。每行第一個widgets與行首對齊,每行最後一個widgets與行尾對齊
  • spaceAround: 均勻分配,相鄰widgets間距離相同。每行第一個widgets到行首的距離和每行最後一個widgets到行尾的距離將會是相鄰widgets之間距離的一半
  • spaceEvenly: 均勻分配,相鄰widgets間距離相同。每行第一個widgets到行首的距離和每行最後一個widgets到行尾的距離和相鄰widgets之間距離相同
屬性 效果
start
start
end
end
center
center
spaceBetween
spaceBetween
spaceAround
spaceAround
spaceEvenly
spaceEvenly

mainAxisSize

// 默認值
MainAxisSize mainAxisSize = MainAxisSize.max
複製代碼
  • 表示Row在主軸(水平)方向佔用的空間,默認是MainAxisSize.max
  • max表示儘量多的佔用水平方向的空間,此時不管子widgets實際佔用多少水平空間,Row的寬度始終等於水平方向的最大寬度;
  • MainAxisSize.min表示儘量少的佔用水平空間,當子widgets沒有佔滿水平剩餘空間,則Row的實際寬度等於全部子widgets佔用的的水平空間

verticalDirection

表示Row縱軸(垂直)的對齊方向, 默認值down,表示從上到下; up表示從下到上git

// 默認值
VerticalDirection verticalDirection = VerticalDirection.down
複製代碼

crossAxisAlignment

  • 表示子Widgets在縱軸方向的對齊方式,Row的高度等於子Widgets中最高的子元素高度
  • crossAxisAlignment的參考系是verticalDirection
// 默認值
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center

/** * VerticalDirection.down時, crossAxisAlignment.start指頂部對齊 * VerticalDirection.up時,crossAxisAlignment.start指底部對齊 * crossAxisAlignment.end和crossAxisAlignment.start正好相反 */
複製代碼
  • VerticalDirection.down時, crossAxisAlignment個枚舉值以下
  • start: 頂部對其
  • end: 底部對其
  • center: 居中對其
  • stretch: 側軸方向上, 子Widget的高度拉伸至和Row的高度相同
  • baseline: 不論VerticalDirection取值如何, 子Widget的頂部和Row的頂部對其

textDirection

表示水平方向子widget的佈局順序(是從左往右仍是從右往左),默認爲系統當前Locale環境的文本方向(如中文、英語都是從左往右,而阿拉伯語是從右往左)github

TextDirection textDirection
/** * ltr: 從左往右 * rtl: 從右往左 */
複製代碼

textBaseline

用於對其文本的水平線, 詳情可參考算法

TextBaseline textBaseline
/** * alphabetic: 用於對齊普通的字母基線 * ideographic: 用於對齊表意基線 */
複製代碼

使用代碼

Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.center,
        textDirection: TextDirection.ltr,
        verticalDirection: VerticalDirection.down,
        textBaseline: TextBaseline.ideographic,
        children: <Widget>[
          new Container(width: 80.0, height:80.0, color: Colors.red,),
          new Container(width: 80.0, height:90.0, color: Colors.green,),
          new Container(width: 80.0, height:100.0, color: Colors.blue,),
        ],
    )
複製代碼

特別注意canvas

RowColumn中, 若是子widget超出屏幕範圍,則會報溢出錯誤數組

image

彈性佈局

  • 彈性佈局容許子widget按照必定比例來分配父容器空間
  • Flutter中的彈性佈局主要經過FlexExpanded來配合實現
  • Flex能夠沿着水平或垂直方向排列子widget
  • 若是已知主軸方向,建議使用RowColumn,由於RowColumn都繼承自Flex,參數基本相同,因此能使用Flex的地方必定可使用RowColumn
  • Flex自己功能是很強大的,它也能夠和Expanded配合實現彈性佈局,接下來咱們只討論Flex和彈性佈局相關的屬性(其它屬性已經在介紹RowColumn時介紹過了)

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>[],
})

// direction
// 水平方向
Axis direction = Axis.horizontal
// 垂直方向, 默認爲垂直方向
Axis direction = Axis.vertical
複製代碼

Flex繼承自MultiChildRenderObjectWidget,對應的RenderObjectRenderFlexRenderFlex中實現了其佈局算法微信

Expanded

能夠按比例縮放RowColumnFlexwidget所佔用的空間markdown

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

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

Row(
        children: <Widget>[
          Container(width: 80.0, height:80.0, color: Colors.red,),
          Expanded(
            flex: 1,
            child: Container(width: 80.0, height:80.0, color: Colors.blue,),
          ),
          Expanded(
            flex: 1,
            child: Container(width: 80.0, height:80.0, color: Colors.yellow,),
          )
        ],
      ),
複製代碼

流式佈局

  • 上面提到在RowColumn中, 若是子widget超出屏幕範圍,則會報溢出錯誤
  • 這是由於Row默認只有一行,若是超出屏幕不會折行
  • 咱們把超出屏幕顯示範圍會自動折行的佈局稱爲流式佈局
  • Flutter中經過WrapFlow來支持流式佈局

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>[],
})
複製代碼

能夠看到Wrap中的不少屬性和Row中相同, 這裏就不在贅述了, 這裏主要看一下Wrap中特有的屬性ide

alignment

Widget在主軸上的對其方式

// 默認值
this.alignment = WrapAlignment.start
// 取值: start, end, center, spaceBetween, spaceAround, spaceEvenly
複製代碼

runAlignment

Widget在縱軸上的對其方式

// 默認值
this.runAlignment = WrapAlignment.start
// 取值: start, end, center, spaceBetween, spaceAround, spaceEvenly
複製代碼

spacing

主軸方向子widget的間距: spacing: 10

runSpacing

縱軸方向子widget的間距: runSpacing: 10

Flow

  • 通常不多會使用Flow,由於其過於複雜,須要本身實現子widget的位置轉換,在不少場景下首先要考慮的是Wrap是否知足需求
  • Flow主要用於一些須要自定義佈局的UI或性能要求較高(如動畫中)的場景
  • Flow有以下優勢:
    • 性能好: Flow是一個對child尺寸以及位置調整很是高效的控件,Flow用轉換矩陣對child進行位置調整的時候進行了優化
    • Flow定位事後,若是child的尺寸或者位置發生了變化,在FlowDelegate中的paintChildren()方法中調用context.paintChild 進行重繪,而context.paintChild在重繪時使用了轉換矩陣,並無實際調整Widget位置。
    • 靈活: 因爲咱們須要本身實現FlowDelegatepaintChildren()方法,因此咱們須要本身計算每個widget的位置,所以,能夠實現自定義佈局。
  • 缺點:
    • 使用複雜.
    • 不能自適應子widget大小,必須經過指定父容器大小或重寫FlowDelegategetSize返回固定大小
  • 下面是一個簡單的示例代碼:
class FlowWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      color: Colors.orange,
      child: Flow(
        delegate: ShowFlowDelegate(margin: EdgeInsets.all(10)),
        children: <Widget>[
          Container(width: 100.0, height:100.0, color: Colors.red),
          Container(width: 100.0, height:100.0, color: Colors.yellow),
          Container(width: 100.0, height:100.0, color: Colors.blue),
          Container(width: 100.0, height:100.0, color: Colors.cyan),
          Container(width: 100.0, height:100.0, color: Colors.pink)
        ],
      ),
    );
  }
}
複製代碼

實現一個繼承自FlowDelegate的類, 並重寫響應的方法

class ShowFlowDelegate extends FlowDelegate {
  EdgeInsets margin =EdgeInsets.zero;
  ShowFlowDelegate({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;
      }
    }
  }

  @override
  Size getSize(BoxConstraints constraints) {
    // 設置Flow的大小
    return Size(double.infinity, 300);
  }

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

層疊佈局

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

Stack

Stack({
  Key key,
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})
複製代碼

alignment

決定子WidgetStack中的定位

// 默認值
this.alignment = AlignmentDirectional.topStart

// 取值以下, start和end爲水平方向, top和bottom是垂直方向
static const AlignmentDirectional topStart = AlignmentDirectional(-1.0, -1.0);
static const AlignmentDirectional topCenter = AlignmentDirectional(0.0, -1.0);
static const AlignmentDirectional topEnd = AlignmentDirectional(1.0, -1.0);

static const AlignmentDirectional centerStart = AlignmentDirectional(-1.0, 0.0);
static const AlignmentDirectional center = AlignmentDirectional(0.0, 0.0);
static const AlignmentDirectional centerEnd = AlignmentDirectional(1.0, 0.0);

static const AlignmentDirectional bottomStart = AlignmentDirectional(-1.0, 1.0);
static const AlignmentDirectional bottomCenter = AlignmentDirectional(0.0, 1.0);
static const AlignmentDirectional bottomEnd = AlignmentDirectional(1.0, 1.0);

// 還可使用具體數值比例定位, 設置值在0~1之間
AlignmentDirectional(0.8, 0.9)
複製代碼

textDirection

決定alignment對齊的參考系

// 默認ltr
textDirection: TextDirection.ltr

// textDirection的值爲TextDirection.ltr,則alignment的start表明左,end表明右
// textDirection的值爲TextDirection.rtl,則alignment的start表明右,end表明左
複製代碼

fit

用於決定沒有定位的子widget如何去適應Stack的大小

// 默認值
this.fit = StackFit.loose

// StackFit.loose表示使用子widget的大小
// StackFit.expand表示擴伸到Stack的大小
複製代碼

overflow

決定如何顯示超出Stack顯示空間的子widget

// 默認值
this.overflow = Overflow.clip

// Overflow.clip時,超出部分會被剪裁(隱藏)
// Overflow.visible時,時則不會被剪裁
複製代碼

使用示例

Stack(
  // alignment: AlignmentDirectional.center,
  alignment: AlignmentDirectional(0.8, 0.8),
  textDirection: TextDirection.ltr,
  fit: StackFit.loose,
  overflow: Overflow.visible,
  children: <Widget>[
    Container(width: 100.0, height:100.0, color: Colors.red),
    Container(width: 100.0, height:100.0, color: Colors.yellow),
  ],
)
複製代碼

Positioned

PositionediOS中的Frame設置位置和大小同樣, 根據上下左右和寬高設置Widget的定位和大小

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

left、top 、right、 bottom分別表明離Stack左、上、右、底四邊的距離, widthheight用於指定定位元素的寬度和高度

注意,此處的width、height 和其它地方的意義稍微有點區別,此處用於配合left、top 、right、 bottom來定位widget,舉個例子,在水平方向時,你只能指定left、right、width三個屬性中的兩個,如指定left和width後,right會自動算出(left+width),若是同時指定三個屬性則會報錯,垂直方向同理

child: Stack(
  alignment: AlignmentDirectional.center,
  children: <Widget>[
    // 這個widget會根據alignment的設置展現
    Container(child: Text('https', style: TextStyle(color: Colors.red)), color: Colors.yellow,),
    // 這個widget會根據left和top和width的設置顯示和alignment無關了, 實際width爲80
    Positioned(
      left: 10,
      top: 30,
      width: 80,
      child: Container(width: 100.0, height:100.0, color: Colors.red),
    ),
    Positioned(
      right: 10,
      bottom: 50,
      child: Container(width: 100.0, height:100.0, color: Colors.blue),
    )
  ],
),
複製代碼

至此, Flutter中佈局相關的Widget也都學習完了......接下來就是容器類Widget

參考文獻


歡迎您掃一掃下面的微信公衆號,訂閱個人博客!

微信公衆號
相關文章
相關標籤/搜索