天天看點

經典布局:如何定義子控件在父容器中的排版位置?

在之前的文章中,我們一起學習了建構視圖的基本元素,文本Text、圖檔Image和按鈕,用于展示一組連續視圖元素的ListView,以及處理多重嵌套的可滾動視圖的CustomScrollView,等等。

在Flutter中,一個完整的界面通常就是由這些小型、單用途的基本控件元素依據特定的布局規則堆砌而成的。那麼今天,我們就一起來了解下,在Flutter中,搭建出一個漂亮的布局,我們需要了解哪些布局規則,以及這些規則與其他平台類似概念的差别在哪裡。

我們已經知道,在Flutter中一切皆Widget,那麼布局也不例外。但與基本控件元素不同,布局類的Widget并不會直接呈現視覺内容,而是作為承載其他子Widget的容器。

這些布局類的Widget,内部都會包含一個或多個子控件,并且都提供了擺放子控件的不同布局方式,可以實作子控件的對齊、嵌套、層疊和縮放等。而我們要做的就是,通過各種定制化的參數,将其内部的子Widget按照自己的布局規則放置在特定的位置上,最終形成一個漂亮的布局。

Flutter提供了31種布局Widget,對布局控件的劃分非常詳細,一些相同(或相似)的視覺效果可以通過多種布局控件實作,是以布局類型相比原生iOS、Android平台多了不少。比如Android布局一般就隻有FrameLayout、LinearLayout、RelativeLayout、GridLayout和TableLayout這5種,而iOS的布局更少,隻有Frame布局和自動布局兩種。

今天,我着重介紹幾類在開發Flutter應用時,最常用也最具有代表性的布局Widget,包括單子Widget布局、多子Widget布局、層疊Widget布局。掌握這些典型的Widget,你就基本掌握了建構一個界面精美的APP所需要的全部布局方式了。接下來,我們就先從單子Widget聊起吧。

單子Widget布局:Container、Padding和Center

單子Widget布局類容器比較簡單,一般用來對其唯一的子Widget進行樣式包裝,比如限制大小、添加背景色樣式、内間距、旋轉變換等。這一類布局Widget,包括Container、Padding與Center三種。

Container,是一種允許在其内部添加其他控件的控件,也是UI架構中的一個常見概念。

在Flutter中,Container本身可以單獨作為控件存在(比如單獨設定背景色、寬高),也可以作為其他控件的父級存在:Container可以定義布局過程中子Widget如何擺放,以及如何展示。與其他架構不同的是,Flutter的Container僅能包含一個子Widget。

是以,對于多個子Widget的布局場景,我們通常會這樣處理:先用一個根Widget去包含這些子Widget,然後把這個根Widget放到Container中,再由Container設定它的對齊alignment、邊距padding等基礎屬性和樣式屬性。

接下來,我通過一個示例,與你示範如何定義一個Container。

在這個示例中,我将一段較長的文字,包裝在一個紅色背景、圓角邊框、固定寬高的Container中,并分别設定了Container的外邊距(距離其父Widget的邊距)和内邊距(距離其子Widget的邊距):

Container(
      child: Text("Container(容器)在UI架構中是一個很常見的概念,Flutter也不例外!~~"),
      width: 180,
      height: 240,
      margin: EdgeInsets.all(44),//外邊距
      padding: EdgeInsets.all(18),//内邊距
      alignment: Alignment.center,//子Widget居中對齊
      decoration: BoxDecoration(
        color: Colors.red,//背景色
        borderRadius: BorderRadius.circular(10),//圓角邊框
      ),
    );           

複制

經典布局:如何定義子控件在父容器中的排版位置?

如果我們隻需要将子Widget設定間距,則可以使用另一個單子容器控件Padding進行内容填充:

Padding(
      child: Text("Container(容器)在UI架構中是一個很常見的概念,Flutter也不例外!~~"),
      padding: EdgeInsets.all(44),
    );           

複制

經典布局:如何定義子控件在父容器中的排版位置?

在需要設定内容間距時,我們可以通過EdgeInsets的不同構造函數,分别制定四個方向的不同補白方式,如均使用同樣數值留白(EdgeInsets.all),隻設定左留白(EdgeInsets.only)或對稱方向留白(EdgeInsets.symmetric)等。

接下來,我們再來看看單子Widget布局容器中另一個常用的容器Center。正如它的名字一樣,Center會将對其子Widget居中排列。

比如,我可以把一個Text包在Center裡,實作居中展示:

Center(
      child: Text("Center"),
    );           

複制

經典布局:如何定義子控件在父容器中的排版位置?

需要注意的是,為了實作居中布局,Center所占據的空間一定要比其子Widget要大才行,這也是顯而易見的:如果Center要和其子Widget一樣大,自然就不需要居中,也沒空間居中了。是以Center通常會結合Container一起使用。

現在,我們結合Container,一起看看Center的具體使用方法吧。

Container(
      child: Center(
        child: Text("Container(容器)在UI架構中是一個很常見的概念,Flutter也不例外!~~~"),
      ),
      height: 240,
      width: 180,
      padding: EdgeInsets.all(18),
      margin: EdgeInsets.all(44),
      decoration: BoxDecoration(
        color: Colors.red,
        borderRadius: BorderRadius.circular(10),
      ),
    );           

複制

可以看到,我們通過Center容器實作了Container容器中 alignment: Alignment.center 的效果。

事實上,為了達到這一效果,Container容器與Center容器底層都依賴了同一個容器Align,通過它實作子Widget的對齊方式。

接下來,我們再來看看多子Widget布局的三種方式,即Row、Column與Expanded。

多子Widget布局:Row、Column和Expanded

對于擁有多個子Widget的布局類容器而言,其布局行為無非就是兩種規則的抽象:水準方向上應該如何布局、垂直方向上應該如何布局。

如同Android的LinearLayout、前端的Flex布局一樣,Flutter中也有類似的概念,即将子Widget按行水準排列的Row,按列垂直排列的Column,以及負責配置設定這些子Widget在布局方向中剩餘空間的Expanded。

Row與Column的使用方法很簡單,我們隻需要将各個子Widget按序加入到Children數組即可。在下面的代碼中,我把四個分别設定了不同顔色和寬高的Container加到Row與Column中:

//Row的用法示範
Row(
      children: <Widget>[
        Container(color: Colors.red, height: 80, width: 60),
        Container(color: Colors.yellow, height: 80, width: 60),
        Container(color: Colors.blue, height: 180, width: 100),
        Container(color: Colors.green, height: 80, width: 60),
      ],
    );
    
//Column的用法示範
Column(
      children: <Widget>[
        Container(color: Colors.red, height: 80, width: 60),
        Container(color: Colors.yellow, height: 80, width: 60),
        Container(color: Colors.blue, height: 180, width: 100),
        Container(color: Colors.green, height: 80, width: 60),
      ],
    );           

複制

Row的顯示效果如下:

經典布局:如何定義子控件在父容器中的排版位置?

Column的顯示效果如下:

經典布局:如何定義子控件在父容器中的排版位置?

可以看到,單純使用Row和Column控件,在子Widget的尺寸較小時,無法将容器填滿,視覺樣式比較難看。對于這樣的場景,我們可以通過Expanded控件,來制定配置設定規則填滿容器的剩餘空間。

比如,我們希望Row元件(或者Column元件)中的綠色容器與黃色容器均分剩下的空間,于是就可以設定他們的彈性系數參數flex都為1,這兩個Expanded會按照其flex的比例(即1:1)來分割剩餘的Row橫向(Column縱向)空間:

Row(
      children: <Widget>[
        Expanded(flex: 1, child: Container(color: Colors.red, height: 80, width: 60)),
        Container(color: Colors.yellow, height: 80, width: 60),
        Container(color: Colors.blue, height: 180, width: 100),
        Expanded(flex: 1, child: Container(color: Colors.green, height: 80, width: 60)),
      ],
    );           

複制

經典布局:如何定義子控件在父容器中的排版位置?

于Row和Column而言,Flutter提供了依據坐标軸的布局對齊行為,即根據布局方向劃分出主軸和交叉軸:主軸,表示容器依次擺放子Widget的方向;交叉軸,則是與主軸垂直的另一個方向。

比如,Row的主軸是橫向,交叉軸是縱向;Column的主軸是縱向,交叉軸是橫向。

我們可以根據主軸和交叉軸,設定子Widget在這兩個方向上的對齊規則mainAxisAlignment與crossAxisAlignment。比如,對于Row而言,主軸方向start表示靠左對齊、center表示橫向居中對齊,end表示靠右對齊,spaceEvenly表示按固定間距對齊;而交叉軸方向start則表示靠上對齊,center表示縱向居中對齊,end表示靠下對齊。

下圖展示了在Row中設定不同方向的對齊規則後的呈現效果:

Row的主軸對齊方式

經典布局:如何定義子控件在父容器中的排版位置?

Row的縱軸對齊方式:

經典布局:如何定義子控件在父容器中的排版位置?

Column的對齊方式也是類似的,這裡不做過多展開。

需要注意的是,對于主軸而言,Flutter預設是讓父容器決定其長度,即盡可能大。

在上例中,Row的寬度為螢幕寬度,Column的高度為螢幕高度。主軸長度大于所有子Widget的總長度,意味着容器在主軸方向的空間比子Widget要大,這也是我們能通過主軸對齊方式設定子Widget布局效果的原因。

如果想讓容器與子Widget在主軸上完全比對,我們可以通過設定Row的mainAxisSize參數為MainAxisSize.min,由所有子Widget來決定主軸方向的容器長度,即主軸方向的長度盡可能小。

Row(
      mainAxisSize: MainAxisSize.min,//讓容器寬度與所有子Widget的寬度總和一緻
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,//由于容器與子Widget一樣寬,是以這行設定排列間距的代碼并未起作用
      children: <Widget>[
        Container(color: Colors.red, height: 80, width: 60),
        Container(color: Colors.yellow, height: 80, width: 60),
        Container(color: Colors.blue, height: 180, width: 100),
        Container(color: Colors.green, height: 80, width: 60)
      ],
    );           

複制

經典布局:如何定義子控件在父容器中的排版位置?

可以看到,我們設定了主軸大小為MainAxisSize.min之後,Row的寬度變得和其子Widget一樣大,是以再設定主軸的對齊方式也就不起作用了。

層疊Widget布局:Stack與Positioned

有些時候,我們需要讓一個控件疊加在另一個控件的上面,比如在一張圖檔上放置一段文字,又或是在圖檔的某個區域放置一個按鈕。這時候,我們就需要用到層疊布局容器Stack了。

Stack容器與前端中的絕對定位、iOS中的Frame布局非常類似,子Widget之間允許疊加,還可以根據父容器上下左右四個角的位置來确定自己的位置。

Stack提供了層疊布局的容器,而Positioned則提供了設定子Widget位置的能力。接下來,我們通過一個例子來看一下Stack和Position的用法吧。

在這個例子中,我先在Stack中放置了一塊300x300的黃色畫布,随後在(18,18)處放置了一個50x50的綠色控件,然後在(18,70)處放置了一個文本控件。

Stack(
      children: <Widget>[
        Container(
          height: 300,
          width: 300,
          color: Colors.yellow,
        ),
        Positioned(
          left: 18,
          top: 18,
          child: Container(height: 50, width: 50, color: Colors.green),
        ),
        Positioned(
          left: 18,
          top: 70,
          child: Text("文本控件"),
        )
      ],
    );           

複制

運作一下,可以看到,這三個子Widget都按照我們預定的規則疊加在一起了。

經典布局:如何定義子控件在父容器中的排版位置?

Stack控件允許其子Widget按照建立的先後順序進行層疊擺放,而Position控件則用來控制這些子Widget的擺放位置。需要注意的是,Positioned控件隻能在Stack中使用,在其他容器中使用會報錯。

總結

Flutter的布局容器強大而豐富,可以将小型、單用途的基本視覺元素快速封裝成控件。

單子容器包括Container、Padding和Center。其中,Container内部提供了間距、背景樣式等基礎屬性,為子Widget的擺放方式,及展現樣式都提供了定制能力。而Padding與Center提供的功能,則正如其名一樣簡潔,就是對齊與居中。

多子Widget布局有Row和Column,使用Expanded控件使用容器内部的剩餘空間。

層疊布局Stack,以及與之搭配使用的,定位子Widget位置的Positioned容器,通過它們,實作多控件堆放的布局效果。

以上