Flutter | 佈局元件

345丶發表於2021-03-02

佈局類元件都會包含一個或多個元件,不同的佈局類元件對子元件(layout)方式不同。在 Flutter 中 Element 樹才是最終的繪製樹,Element 樹是通過 Widget 樹來建立的 (通 Widget.createElement()) ,Widget 其實就是 Element 的配置資料。

在 Fluter 中,根據 Widget 是否需要包含子節點將 Widget 分為了三類,分別對應三種 Element,如下表:

Widget對應的 Element用途
LeafRenderObjectWidgetLeafRenderObjectElementwidget 樹的葉子節點,用於沒有子節點的 Widget,通過基礎元件都屬於這一類,如 Image等
SingleChildRenderObjectWidgetSingleChindRenderObjectElement包含一個 子 Widget,如:ConstrainedBox,DecoratedBox等
MultiChildRenderObjectWidgetMultiChildRenderObjectElement包含多個子Widget,一般都有一個 children 引數,接收一個 Widget 陣列,如 Row,Column,Stack 等

Flutter 中很多 widget 都是繼承自 StatelessWidget 或者 StatefulWidget ,然後再 build 方法中構建真正的 RenderObjectWidget。如 Text 是繼承自 StatelessWidget ,然後在 build 方法中通過 RichText 構建子樹,而 RichText 才是繼承自 MultiChildRenderObjectWidget。

所以說 Text 屬於 MultiChildRenderWidget(其他 Widget 也可以這樣描述),其實 StatelessWidget 和 StatefulWidget 就是兩個用於組合的 Widget 的基類,他們本身最終並不關聯最終的渲染物件(RenderObjectWidget)

MultiChildRenderObjectWidget 是繼承自 RenderObjectWidget 的,在 RenderObjectWidget 中定義了建立,更新 RenderObject 的方法,子類必須實現他們,其實 RenderObject 就是最終佈局,渲染 UI 介面的物件,也就是說,對於佈局類元件來說,其佈局演算法都是通過對應的 RenderObject 物件來實現的。

所以在 RichText 中就實現了 建立,更新 RenderObject 的方法

佈局元件就是直接或間接繼承(包含)MultiChildRenderObjectWidget 的 Widget,他們一般都會有一個 children 屬性用於接收子 Widget 。

一個普通的 Widget 繼承路線為:

繼承 (Stateless/Stateful)Widget ,然後實現 build 方法

在 build 方法中通過建立繼承自 (Leaf/SingleChild/MultiChild)RenderObjectWidget 的類,然後實現對應的方法來構建最終的渲染UI介面的物件(RenderObject)

(Leaf/SingleChild/MultiChild)RenderObjectWidget 則是繼承自 RenderObjectWidget ,最終繼承自 Widget。

在 RenderObjectWidget 和 Widget 中定義著 建立,更新RenderObject 的方法。以及 createElement 方法。

其實 createElement 方法是在 (Leaf/SingleChild/MultiChild)RenderObjectWidget 類中實現的,而建立,更新 ObjectRender 則是在 (Leaf/SingleChild/MultiChild)RenderObjectWidget 的實現類中完成的


線性佈局(Row 和 Column)

線性佈局指的是沿著水平或者垂直方向排布子元件。在 Flutter 中通過 Row 和 Column 來實現線性佈局,類似於 Android 中的 LinearLayout 控制元件

Row 和 Column 都繼承子 Flex,至於 Fiex 暫不多說

主軸和縱軸

線上性佈局中,如果佈局是水平方向,主軸就是指水平方向,縱軸即垂直方向;如果佈局是垂直方向,主軸就是垂直方向,那麼縱軸就是水平方向。

線上性佈局中,有兩個定義對齊方式的列舉類 MainAxisAlignmentCrossAxisAlignment ,分別代表主軸對齊和縱軸對齊

Row

Row 可以在水平方向排列子 Widget。其定義如下:

Row({
  //......
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection? textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline textBaseline = TextBaseline.alphabetic,
  List<Widget> children = const <Widget>[],
})
複製程式碼
  • textDirection :水平方向元件的佈局順序,預設為系統當前 Locale 環境的文字方向(中文,英語都是左往右,而阿拉伯是右往左)

  • mainAxisSize:表示 Row 在主軸(水平)佔用的空間,如 MainAxisSize.max 表示儘可能多的佔用水平方向的空間,此時無論子 Widget 佔用多少空間,Row 的寬度始終等於水平方向的最大寬度; MainAxisSize.min 表示儘可能的少佔用水平空間,當子 Widget 沒有佔滿水平剩餘空間,則 Row 的實際寬度等於所有的子元件佔用的水平空間。

    其實就相當於 Android 中的 match_parentwarp_parent

  • mainAxisAlignment:表示子元件在 Row 所佔水平空間的對齊方式,如果 mainAxisSize 值為 min,則此屬性毫無意義,對應的值有 start ,center,end 等。

    需要注意的是,textDirectionmainAxisAlignment 的參考系。例如 textDirectiontextDirection.ltr 時,則 MainAxisAlignment.start 表示左對齊,如果為 rtl 則,start 表示右對齊

  • crossAxisAlignment:表示子元件在縱軸的對齊方式,他的值也是 start,center,end 。只不過參考系是 verticalDirection 的值,具體的和上面的差不多,只是方向變了

  • children:子元件陣列

栗子

class RowTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("線性佈局 Row,Column"),
      ),
      body:  Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.end,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            textDirection: TextDirection.rtl,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            verticalDirection: VerticalDirection.up,
            children: [
              Text(
                "Hello word",
                style: TextStyle(fontSize: 30),
              ),
              Text("345")
            ],
          )
        ],
      )
    );
  }
}
複製程式碼

image-20210224142223453

Column

Column 可以在垂直方向排列其子元件,引數和 Row 一樣,只不過排列的方式是垂直的,主軸和縱軸相反。

栗子

class ColumnTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("線性佈局 Row,Column"),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [Text("Hi"), Text("World")],
      ),
    );
  }
}
複製程式碼

image-20210224145812698

由於有指定 主軸的 size,所以預設為 max。則這個 Column 會佔用盡可能多的空間,這個栗子中為螢幕的高度

crossAxisAlignment 為 center,表示在縱軸上居中對齊。Colum 的寬度取決於其子 Widget 中寬度最大的 Widget,所以 hi 會被顯示在 world 的中間部分

RowColumn 都只會在主軸上佔用儘可能的最大空間,而縱軸的長度取決於他們最大子 Widget 的長度

如何讓 hi 和 world 在螢幕中間對齊呢,有如下兩種辦法:

class ColumnTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("線性佈局 Row,Column"),
      ),
      body: ConstrainedBox(
        constraints: BoxConstraints(minWidth: double.infinity),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [Text("Hi"), Text("World")],
        ),
      ),
    );
  }
}
複製程式碼

特殊情況

如果 Row 巢狀 Row ,或者 Column 巢狀 Column,那麼之後最外面的 Row/Column 會佔用盡可能大的空間,

class ColumnTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("線性佈局 Row,Column"),
        ),
        body: Container(
          color: Colors.green,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.max, //有效,外層Colum高度為整個螢幕
              children: <Widget>[
                Container(
                  color: Colors.red,
                  child: Column(
                    mainAxisSize: MainAxisSize.max, //無效,內層Colum高度為實際高度
                    children: <Widget>[
                      Text("hello world "),
                      Text("I am Jack "),
                    ],
                  ),
                )
              ],
            ),
          ),
        ));
  }
}
複製程式碼
image-20210224153808126

這種情況可以使用 Expanded 元件

children: <Widget>[
 Expanded(
   child:  Container(
     color: Colors.red,
     child: Column(
       mainAxisSize: MainAxisSize.max, //無效,內層Colum高度為實際高度
       children: <Widget>[
         Text("hello world "),
         Text("I am Jack "),
       ],
     ),
   ),
 )
]
複製程式碼
image-20210224154043545

彈性佈局 Flex

彈性佈局允許子元件按照一定比例來分配父容器空間。Flutter 中彈性佈局主要通過 Flex 和 Expanded 來配合實現

Flex 元件可以沿著水平或者垂直方向排列子元件,如果知道主軸方向,使用 Row 或者 Column 會更方便一些。Row 和 Column 都繼承子 Flex,引數也都基本相同,所以能使用 Flex 的地方基本上都可以使用 Row 或者 Column。

Flex 可以和 Expanded 元件配合實現彈性佈局,大多數引數基本都和線性佈局一樣,這裡不做介紹,定義如下

Flex({
  Key? key,
  required this.direction,
  List<Widget> children = const <Widget>[],
}) 
複製程式碼
  • direction:彈性佈局的方向,Row 預設為 水平方向,Column 預設為垂直方向

Flex 繼承自 MultiChildRenderObjectWidget ,對應的 RenderObject 為 RenderFlex,RenderFlex 中實現了其佈局演算法

Expanded

可以按比例 擴伸 Row,Column 和 Flex 子元件所佔用的空間

const Expanded({
  int flex = 1, 
  @required Widget child,
})
複製程式碼
  • flex:彈性係數,如果為 0 或者 null,則沒有彈性,既不會擴充套件佔用的空間。如果大於 0,所有的 Expanded 按照 flex 的比例來分隔主軸的全部空閒空間

栗子

class FlexTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("彈性佈局Flex"),
      ),
      body: Column(
        children: [
          Flex(
            direction: Axis.horizontal,
            children: [
              Expanded(
                flex: 1,
                child: Container(
                  height: 30,
                  color: Colors.red,
                ),
              ),
              Expanded(
                flex: 2,
                child: Container(
                  height: 30,
                  color: Colors.blue,
                ),
              )
            ],
          ),
          Padding(
            padding: const EdgeInsets.only(top: 20),
            child: SizedBox(
              height: 100,
              child: Flex(
                direction: Axis.vertical,
                children: [
                  Expanded(
                    flex: 2,
                    child: Container(
                      height: 30,
                      color: Colors.yellow,
                    ),
                  ),
                  Spacer(
                    flex: 1,
                  ),
                  Expanded(
                    flex: 1,
                    child: Container(
                      height: 30,
                      color: Colors.green,
                    ),
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}
複製程式碼

效果如下所示:

image-20210224222520285

栗子中的 Spacer 的功能是佔用指定比例的空間,實際上它只是 Expanded 的一個包裝類


流式佈局 Wrap ,Flow

在使用 Row 和 Column 時,如果子 Widget 超出 螢幕範圍,則會報溢位錯誤,如:

class WrapAndFlowTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("WarpAndFlow"),
      ),
      body: Container(
        height: 100,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [Text("345" * 100)],
        ),
      ),
    );
  }
}
複製程式碼
image-20210224223951045

可以看到,右邊部分報出溢位錯誤。這是因為 Row 預設只有一行,如果超出螢幕,不會折行,並且會報錯

我們把超出自動折行的佈局稱為流式佈局。Flutter 中通過 Wrap 和 Flow 來支援流式佈局。

Wrap 定義如下

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 ,Colum 中都有,如 direction,textDirection等,這些引數意義都相同,這裡不過多介紹

  • spacing:主軸方向子 Widget 的間距
  • runSpacing:縱軸方向的間距
  • runAlignment:縱軸方向的對齊方式

例子

class WrapAndFlowTest extends StatelessWidget {
  final List<String> _list = const [
    "愛是你我",
    "一壺老酒",
    "最炫民族風",
    "怒放的生命",
    "再見青春",
    "北京,北京"
  ];

  List<Widget> getMusicList() {
    /* List<Widget> widgets = new List();
    _list.forEach((element) {
      widgets.add(RaisedButton(
        child: Text(element),
        onPressed: () => print(element),
      ));
    });*/

    return _list
        .map((e) => RaisedButton(
              child: Text(e),
              onPressed: () => print(e),
            ))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("WarpAndFlow"),
        ),
        body: Padding(
          padding: EdgeInsets.all(10),
          child: Flex(
            direction: Axis.horizontal,
            children: [
              Expanded(
                flex: 1,
                child: Wrap(
                  spacing: 25,
                  runSpacing: 4,
                  alignment: WrapAlignment.center,
                  children: getMusicList(),
                ),
              )
            ],
          ),
        ));
  }
}
複製程式碼
image-20210224234240783

Flow

我們一般情況下很少使用 Flow,因為其比較複雜,需要手動對 widget 進行佈局,相當於是 android 中的 onLayout 方法。

Flow 主要用於以下需要高度自定義佈局或者效能要求較高(如動畫中) 的場景,

Flow 有如下優點

  • 效能好:Flow 是一個隊子元件尺寸以及位置調整非常高效的控制元件。Flow 用轉換矩陣對子元件進行位置調整的時候進行了優化:在 Flutter 定位過後,如果子元件尺寸發生了變化,在 FlowDelegate 中的 paintChildren() 方法中呼叫 context.paintChild 進行重繪,而 contextPaintChild 進行重繪的時候使用了轉換矩陣,並沒有實際調整元件的位置

  • 靈活:由於需要自定實現 FlowDelegate 的 parintChildren() 方法,所以我們需要手動計算每一個元件的位置,因此,可以自定義佈局策略

缺點

  • 使用複雜
  • 不能自適應子元件大小,必須通過指定父容器大小或者實現 TestFlowDelegate 的 getSize 返回固定大小

示例

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 width = context
          .getChildSize(i)
          .width + x + margin.right;
      //是否需要換行
      print('$width ---- ${context.size.width}');
      if (width < context.size.width) {
        //繪製第一個
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0));
        x = width + margin.left;
      } else {
        //繪製後面的
        x = margin.left;
        y += context
            .getChildSize(i)
            .height + margin.top + margin.bottom;
        //繪製子widget(有優化)  
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0));
        x += context
            .getChildSize(i)
            .width + margin.left + margin.right;
      }
    }
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return Size(double.infinity, 200);
  }
}
複製程式碼
class WrapAndFlowTest extends StatelessWidget {
  final List<String> _list = const [
    "愛是你我",
    "一壺老酒",
    "最炫民族風",
    "怒放的生命",
    "再見青春",
    "北京,北京"
  ];

  List<Widget> getMusicList() {
    return _list
        .map((e) => Container(
              width: 140,
              height: 40,
              child: RaisedButton(
                child: Text(e),
                onPressed: () => print(e),
              ),
            ))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("WarpAndFlow"),
        ),
        body: Padding(
          padding: EdgeInsets.all(10),
          child: Flow(
            delegate: TestFlowDelegate(margin: EdgeInsets.all(10)),
            children: getMusicList(),
          ),
        ));
  }
}
複製程式碼
image-20210225002701361

可以看到主要的任務就是實現 paintChildren,他的主要任務就是確定每個子 Widget 的位置,由於 Flow 不能自適應 Widget 的大小,所以在 getSize 中返回一個固定大小來指定 Flow 的大小


層疊佈局 Stack,Positioned

層疊佈局和 Android 中的 FrameLayout 佈局是相似的,子元件可以通過父容器的四個角的位置來確定自身的位置。

絕對定位允許子元件堆疊起來(按照程式碼中宣告的順序)。Flutter 中使用 Stack 和 Positioned 這兩個 元件來配合實現決定定位。

Stack 允許元件堆疊,而 Positioned 用於根據 Stack 的四個角來確定子元件的位置

Stack

Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})
複製程式碼
  • alignment:此引數決定如何去對齊沒有定位**(沒有使用 Positioned)** 或部分定位的子元件。

    部分定位指的是沒有在某一個軸上定位leftright 為橫軸,topbottom 為縱軸,只要包含某個軸上的一個定位屬性就算在該軸上有定位

  • textDirection:和 RowColumn 中的 textDirection 功能一樣,都用於確定 alignment 對齊的參考系,即 textDirection 值為 ltr,則 alignment 代表左,end 為右。如果 textDirecion為 rtl,start 則為 有,end 為左

  • fit:此引數用於確定沒有定位的子元件如何使用 Stack 的大小。StackFit.loose 表示使用子元件的大小, expand 表示擴伸到 Stack 的大小

  • overflow:此屬性決定如何顯示超出 Stack 顯示空間的子元件;值為 Overflow.clip 時,超出部分會被剪裁(隱藏),只為 Overflow.visible 則不會

Positioned

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

lefttoprightbottom 分別代表 tack 四個邊的距離,widget 耦合 height 用於指定需要定位元素的寬度和高度。

注意,Positioned 的 widget,height 是用於配合 left,top ,right,bottom 來定位元件,舉個例子,在 水平方向是,只能指定 left,right,width 三個屬性的兩個,如指定 left 和 widget 後,right 會自動推算出 (left+widget),如果同時指定三個屬性則會報錯,垂直方向同理

栗子:

class StackAndPositionedTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("StackAndPositioned"),
        ),
        ///通過 ConstrainedBox 來確保 Stack 佔滿螢幕
        body: ConstrainedBox(
          constraints: BoxConstraints.expand(),
          child: Stack(
            alignment: Alignment.center,
            children: [
              Container(
                child: Text(
                  "hello world",
                  style: TextStyle(color: Colors.white),
                ),
                color: Colors.red,
              ),
              Positioned(
                left: 18,
                child: Text("I am 345"),
              ),
              Positioned(
                top: 18,
                child: Text("your friend"),
              )
            ],
          ),
        ));
  }
}
複製程式碼
image-20210301213059690

程式碼中第一個元件 hellow world 沒有使用 Positioned 元件,所以 會受到 Aligment.center 的約束,所以他在圖中是居中顯示的。

第二個子元件 I am 345 只指定了 水平方位 left,屬於部分定位,即垂直沒有定位,那麼他在垂直方向上會按照 aligment 進行對齊,即為垂直居中

第三個 your friend 和 第二個一樣,只不過是制定了 垂直 top,沒有水平定位,則水平方向居中

修改程式碼如下:

 Stack(
  alignment: Alignment.center,
  fit: StackFit.expand,
  children: [
    Positioned(
      left: 18,
      child: Text("I am 345"),
    ),
    Container(
      child: Text(
        "hello world",
        style: TextStyle(color: Colors.white),
      ),
      color: Colors.red,
    ),
    Positioned(
      top: 18,
      child: Text("your friend"),
    )
  ],
)
複製程式碼

上面使用了 fit 屬性,並且是 expand,表示沒有使用定位的子元件會擴伸到 Stack 的大小

由於第二個子元件的寬高和 Stack 一樣大,所以就會導致第一個元件被覆蓋

第三個元件在最上層,正常顯示


對齊與相對定位 Align

通過 StackPositioned 可以指定一個或多個子元件相對於父元素的各個邊進行精確偏移,並且可以重疊,

但是如果只想簡單調整一個子元件在父元素中的位置的話,使用 Align 元件會更簡單一些

Align

Align({
  Key key,
  this.alignment = Alignment.center,
  this.widthFactor,
  this.heightFactor,
  Widget child,
})
複製程式碼

Align 元件可以調整子元件的位置,並根據子元件的寬高來確定自身的寬高

  • aligment:需要一個 AlignmentGeometry 型別的值,表示子元件在父元件中的起始位置,AlignmentGeometry 是一個抽象類,常用的有兩個子類 Aligment 和 FractionalOffset
  • widthFactor 和 heightFactor 用於確定 Align 自身的寬高屬性;他們是兩個縮放因子,分別會乘以子元件的寬高,最終的結果就是 Align 的寬高,如果為 null,則元件的寬高會佔用盡可能多的空間

栗子

class AlignTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Align"),
      ),
      body: Container(
        height: 120,
        width: 120,
        color: Colors.blue[50],
        child: Align(
          alignment: Alignment.topRight,
          child: FlutterLogo(
            size: 60,
          ),
        ),
      ),
    );
  }
}
複製程式碼
image-20210301215833944

FlutterLogo 是 Fluter sdk 的一個元件,內容就是 Flutter 的商品

在 Container 中 制定了 寬高為 120,如果不指定 Container 的寬高,同時指定 widthFactor 和 heightFactor 為 2也可以達到相同的效果

Alignment.topRight 表示子元件的位置為 頂部右上角,具體的值為 Aligment(1,-1)

Aligment

Aligment 繼承自 AligmentGemetry,表示矩形內的一個點,他有兩個屬性 x,y,分別代表水平和垂直的偏移,定義如下:

Alignment(this.x, this.y)
複製程式碼

Aligment 會以矩形的中心點作為座標的原點(Aligemtn(0.0,0.0)), x,y 的值從 -1 到 1, 分別代表矩形從左到右的距離 和 頂部 到底邊的距離。因此 2 個水平/垂直 單位則等於 矩形的寬/高

如 Aligment(-1,-1) 代表左側頂點,1,1代表 右側底部終點;1,-1,則是右側頂點,即為 Aligment.topRight。 為了使用方便,矩形的原點,四個頂點都已經在 Aligment 中定義了靜態常量。

Aligment 可以通過其 座標轉換公式將其座標轉為子元素的具體偏移座標:

偏移量 = (Aligment.x * childWidth/2 + childWidth /2 , Aligment.y * chilHeight/2 + childHeight /2)
複製程式碼

其中 childWidth 為子元素的寬度,childHeight 為子元素的高度

回過頭在看一下上面的栗子,我們將 Aligment(1,-1) 帶入上面的公式,可知 FlutterLogo 的偏移座標正是 (60,0)

修改上面的栗子如下:

class AlignTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Align"),
      ),
      body: Container(
        // height: 120,
        // width: 120,
        color: Colors.blue[50],
        // child: Align(
        //   alignment: Alignment.topRight,
        //   child: FlutterLogo(
        //     size: 60,
        //   ),
        // ),
        child: Align(
          // 2x60/2+60/2 ,0x60/2 + 60/2
          //=90           =30
          alignment: Alignment(2, 0),
          widthFactor: 2,
          heightFactor: 2,
          child: FlutterLogo(
            size: 60,
          ),
        ),
      ),
    );
  }
}
複製程式碼

根據程式碼中註釋的計算,可以得出 x偏移 90,y 偏移30,結果如下:

image-20210301223109448

FractionalOffset

FractionalOffset 繼承自 Aligment , 他和 Aligment 的唯一區別就是座標點不同

FactionalOffset 的座標原點為矩形左側頂點,這和系統佈局一直,所以理解比較容易一點。他的座標轉換公式為:

偏移=(FractionalOffset.x*childWidth , FractionalOffset * childHeight)
複製程式碼

小栗子:

body: Container(
  height: 120,
  width: 120,
  color: Colors.blue[50],
  child: Align(
    alignment: FractionalOffset(0.2, 0.6),
    // 0.2 *60 , 0.6 * 60
    child: FlutterLogo(
      size: 60,
    ),
  ),
)
複製程式碼

帶入公式,偏移量為 (12,60),結果如下:

image-20210301224911296

Align 和 Stack 對比

Align 和 Stack/Positioned 都可以用於指定子元素相對於父元素的偏移,他們主要區別如下

  • 定位參考系統不同
    • Stack/Positioned 定位參考的是父容器的四個頂點
    • Align 則需要先通過引數 alignment 來確定具體的座標,最終的偏移是通過 aligment 的公式計算出的
  • Stack 可以有多個子元素,並且可以堆疊,而 Align 只有一個元素,不存在堆疊

Center 元件

Center 元件用來居中子元素,在之前我們已經使用過他了,下面來介紹一下他,Center 定義如下

class Center extends Align {
  const Center({ Key key, double widthFactor, double heightFactor, Widget child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
複製程式碼

Center 繼承子 Align,它相比 Align 只少了一個 aligment 引數;

由於 Align 中 alignment 值為 center,所以,Center 元件的對齊方式為 Aglinment.center 了

widthFactory 或者 heightFactory 的長度為 null 時表示儘可能佔用更多的空間,這點需要特別注意一下

總結

  • Row / Column

    沿水平或者垂直方向排列子元件

  • Flex

    彈性佈局,個人感覺有點類似於 Android 線性佈局中的 layout_weight 屬性,子元件通過 flex 表示當前元件需要佔總大小的多少。

  • 流式佈局 Wrap/Flow

    Wrap 自動排列,可以指定 對齊屬性等,超過寬度自動折行

    Flow 高度自定義的 Widget,需要手動計算折行位置,排列等,比較適用於高度的自定義

  • 層疊佈局 Stack,Positioned

    Stack 層疊佈局,可以有多個子元件,子元件 Postioned 用於可根據 Stack 的四個角來確定當前元件的位置,沒有使用 Positioned ,則會按照 aligment 進行排列

  • Align

    只能有一個子元件,通過 Aligment / FractionalOffset 進行定位

  • Aligment / FractionalOffset

    兩者都代表這偏移量 ,FractionalOffset 繼承自 Aligment

    Aligment 的原點為 Widget 的中心,即中心點為 Aligment(0,0),具體的偏移可根據公式計算

    FractionalOffset 的原點為Widget 的左上角頂點,即FractionalOffset(0,0),和系統佈局一樣。具體偏移需要公式計算

  • Center

    繼承自 Align,相比與 Align 少了 aligment 引數,該引數預設為居中


參考自 Flutter 實戰

相關文章