flutter系列之:UI layout簡介

簡介

對於一個前端框架來說,除了各個組件之外,最重要的就是將這些組件進行連接的布局了。布局的英文名叫做layout,就是用來描述如何將組件進行擺放的一個約束。

在flutter中,基本上所有的對象都是widget,對於layout來說也不例外。也就是說在flutter中layout也是用代碼來完成的,這和其他的用配置文件來描述layout的語言有所不同。

你可以把layout看做是一種看不見的widget,這些看不見的widget是用來作用在可見的widget對象上,給他們實施一些限制。

flutter中layout的分類

flutter中的layout widget有很多,他們大概可以分為三類,分別是只包含一個child的layout widget,可以包含多個child的layout widget和可滑動的Sliver widgets。

這三種layout也有很多種具體的實現,對於Single-child layout widgets來說,包含下面這些widgets:

  • Align — 用來對其包含在其中的組件進行對其操作。
  • AspectRatio — 對其中的組件進行比例縮放。
  • Baseline — 通過使用子組件的baseline來進行定位。
  • Center — 自組件位於中間。
  • ConstrainedBox — 類似於IOS中的constrain,表示子組件的限制條件。
  • Container — 一個常用的widget,可以用來包含多個其他的widget。
  • CustomSingleChildLayout — 將其單個子項的布局推遲。
  • Expanded — 將Row, Column 或者 Flex的child進行擴展。
  • FittedBox — 根據fit來縮放和定位其child。
  • FractionallySizedBox — 將child按照總可用空間進行調整。
  • IntrinsicHeight — 一個將其child調整為child固有高度的小部件。
  • IntrinsicWidth — 一個將其child調整為child固有寬度的小部件。
  • LimitedBox — 限制一個box的size。
  • Offstage — 將child放入render tree中,但是卻並不觸發任何重繪。
  • OverflowBox — 允許child覆蓋父組件的限制。
  • Padding — 為child提供padding。
  • SizedBox — 給定size的box。
  • SizedOverflowBox — 可以覆蓋父組件限制的box。
  • Transform — 子組件可以變換。

以上是包含單個child的layout組件,下面是可以包含多個child的layout組件:

  • Column — 表示一列child。
  • CustomMultiChildLayout — 使用代理來定位和縮放子組件。
  • Flow — 流式布局。
  • GridView — 網格布局。
  • IndexedStack — 從一系列的child中展示其中的一個child。
  • LayoutBuilder — 可以依賴父組件大小的widget tree。
  • ListBody — 根據給定的axis來布局child。
  • ListView — 可滾動的列表。
  • Row — 表示一行child。
  • Stack — 棧式布局的組件。
  • Table — 表格形式的組件。
  • Wrap — 可以對子child進行動態調整的widget。

可滑動的Sliver widgets有下面幾種:

  • CupertinoSliverNavigationBar — 是一種IOS風格的導航bar。
  • CustomScrollView — 可以自定義scroll效果的ScrollView。
  • SliverAppBar — material風格的app bar,其中包含了CustomScrollView。
  • SliverChildBuilderDelegate — 使用builder callback為slivers提供child的委託。
  • SliverChildListDelegate — 使用list來為livers提供child的委託。
  • SliverFixedExtentList — 固定axis extent的sliver。
  • SliverGrid — child是二維分佈的sliver。
  • SliverList — child是線性布局的sliver。
  • SliverPadding — 提供padding的sliver。
  • SliverPersistentHeader — 可變size的sliver。
  • SliverToBoxAdapter — 包含單個box widget的Sliver。

常用layout舉例

上面我們列出了所有的flutter layout,他們幾乎滿足了我們在程序中會用到的所有layout需求,這裡我們以兩個最基本和最常用的layout:Row和Column為例,來詳細講解layout的使用。

Row和Column都屬於上面講到的多個child的layout widget,它裏面可以包含多個其他的widget組件。

先看一下Row和column的定義。

class Row extends Flex {
  Row({
    Key? key,
    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
    MainAxisSize mainAxisSize = MainAxisSize.max,
    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
    TextDirection? textDirection,
    VerticalDirection verticalDirection = VerticalDirection.down,
    TextBaseline? textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
    List<Widget> children = const <Widget>[],
  }) : super(
    children: children,
    key: key,
    direction: Axis.horizontal,
    mainAxisAlignment: mainAxisAlignment,
    mainAxisSize: mainAxisSize,
    crossAxisAlignment: crossAxisAlignment,
    textDirection: textDirection,
    verticalDirection: verticalDirection,
    textBaseline: textBaseline,
  );
}
class Column extends Flex {
  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>[],
  }) : super(
    children: children,
    key: key,
    direction: Axis.vertical,
    mainAxisAlignment: mainAxisAlignment,
    mainAxisSize: mainAxisSize,
    crossAxisAlignment: crossAxisAlignment,
    textDirection: textDirection,
    verticalDirection: verticalDirection,
    textBaseline: textBaseline,
  );
}

可以看到Row和Column都繼承自Flex,並且他們的構造方法都是調用了Flex的構造方法,兩者的區別就在於direction不同,Row的direction是Axis.horizontal,而Column的direction是Axis.vertical。

那麼什麼是Flex呢?

Flex是一個widget,在Flex中的子組件會按照某一個指定的方向進行展示。這個方向是可以控制的,比如橫向或者豎向,如果你已經提前知道了主軸的方向,那麼可以使用Row或者Column來替代Flex,因為這樣更加簡潔。

在Flex中,如果想要child在某個方向填滿可用空間,則可以將該child包裝在Expanded中。

要注意的是,Flex是不可滾動的,如果Flex中的child太多,超出了Flex中的可用空間,那麼Flex將會報錯,所以如果你需要展示很多child的情況下,可以考慮使用可滾動的組件,比如ListView。

如果你只有一個child,那麼就沒有必要使用Flex或者Row和Column了,可以考慮使用Align或者Center來對child進行定位。

在Flex中有幾個非常重要的參數,比如mainAxisAlignment表示的是子組件沿主軸方向的排列規則,mainAxisSize表示的是主軸的size大小,crossAxisAlignment表示的是和主軸垂直軸的子組件排列規則。當然還有它最最重要的children屬性,children是一個Widget的list列表,用來存儲要展示的子組件。

以Row為例,我們創建一個簡單的RowWidget:

class RowWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      textDirection: TextDirection.ltr,
      children: [
        YellowBox(),
        YellowBox(),
        YellowBox(),
      ],
    );
  }
}

這裡我們返回了一個Row對象,設置了textDirection和children屬性。

children裏面是自定義的YellowBox:

class YellowBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 50,
      height: 50,
      decoration: BoxDecoration(
        color: Colors.yellow,
        border: Border.all(),
      ),
    );
  }
}

YellowBox是一個長和寬都是50的正方形。我們這裡使用了BoxDecoration對其上色。

最後將RowWidget放到Scaffold的body裏面,如下所示:

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: RowWidget()
    );
  }

我們可以看到下面的圖像:

大家可以看到YellowBox是緊貼在一起的,如果我們想要均勻分別該如何做呢?

我們可以在Row中添加一個屬性叫做mainAxisAlignment,取值如下:

mainAxisAlignment: MainAxisAlignment.spaceEvenly

重新運行,生成的圖像如下:

上面我們還提到了一個Expanded組件,可以用來填充剩餘的可用空間,我們把最後一個YellowBox用Expanded圍起來,如下所示:

    return Row(
      textDirection: TextDirection.ltr,
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        YellowBox(),
        YellowBox(),
        Expanded(
          child: YellowBox(),
        )
      ],
    );

生成的圖像如下:

可以看到最後一個Box填充到了整個Row剩餘的空間。

大家要注意,這時候mainAxisAlignment是沒有效果的。

如果觀察Expanded的構造函數,可以看到Expanded還有一個flex屬性:

  const Expanded({
    Key? key,
    int flex = 1,
    required Widget child,
  }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);

flex屬性表示的是flex factor,默認值是1,還是上面的例子,我們將flex調整為2,看看效果:

children: [
        YellowBox(),
        YellowBox(),
        Expanded(
          flex:2,
          child: YellowBox(),
        )
      ],

運行的結果和flex=1是一樣的,為什麼呢?

事實上這個flex表示的是相對於其他Expanded的組件所佔用的空間比例。我們可以講所有的子組件都用Expanded進行擴充,然後再看看效果:

      children: [
        Expanded(
          child: YellowBox(),
        ),
        Expanded(
          child: YellowBox(),
        ),
        Expanded(
          flex: 2,
          child: YellowBox(),
        )
      ],

運行結果如下:

可以看到最後一個child佔用的空間是前面兩個的兩倍。

如果我們想要在YellowBox中間添加空格怎麼辦呢?有兩種方法。

第一種方法是使用SizedBox,如下:

children: [
        Expanded(
          child: YellowBox(),
        ),
        SizedBox(
          width: 100,
        ),
        Expanded(
          child: YellowBox(),
        ),
        Expanded(
          flex: 2,
          child: YellowBox(),
        )
      ],

SizedBox裏面可以包含子child,從而重新設置子child的長度和高度。如果不包含子child則會生成一個空格。

還有一種方式是使用Spacer,如下所示:

      children: [
        Expanded(
          child: YellowBox(),
        ),
        Spacer(flex: 2),
        Expanded(
          child: YellowBox(),
        ),
        Expanded(
          flex: 2,
          child: YellowBox(),
        )
      ],

生成的圖像如下:

Spacer和SizedBox都可以生成空白,不同的是Spacer可以和flex一起使用,而SizedBox必須固定size大小。

總結

以上就是fluter中layout和的分類和基本layout Row和Column的使用情況了。

本文的例子://github.com/ddean2009/learn-flutter.git

更多內容請參考 //www.flydean.com/07-flutter-ui-layout-overview/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!