在前面兩篇文章中,我們一起學習了構建視圖的基本元素:文本、圖片和按鈕,用于展示一組連續視圖元素的 ListView,以及處理多重嵌套的可滾動視圖的 CustomScrollView。
在 Flutter 中,一個完整的界面通常就是由這些小型、單用途的基本控件元素依據特定的布局規則堆砌而成的。那么今天,我就帶你一起學習一下,在 Flutter 中,搭建出一個漂亮的布局,我們需要了解哪些布局規則,以及這些規則與其他平臺類似概念的差別在哪里。希望這樣的設計,可以幫助你站在已有經驗的基礎上去高效學習 Flutter 的布局規則。
我們已經知道,在 Flutter 中一切皆 Widget,那么布局也不例外。但與基本控件元素不同,布局類的 Widget 并不會直接呈現視覺內容,而是作為承載其他子 Widget 的容器。
這些布局類的 Widget,內部都會包含一個或多個子控件,并且都提供了擺放子控件的不同布局方式,可以實現子控件的對齊、嵌套、層疊和縮放等。而我們要做的就是,通過各種定制化的參數,將其內部的子 Widget 依照自己的布局規則放置在特定的位置上,最終形成一個漂亮的布局。
Flutter 提供了 31 種[布局 Widget](https://flutter.dev/docs/development/ui/widgets/layout),對布局控件的劃分非常詳細,一些相同(或相似)的視覺效果可以通過多種布局控件實現,因此布局類型相比原生 Android、iOS 平臺多了不少。比如,Android 布局一般就只有 FrameLayout、LinearLayout、RelativeLayout、GridLayout 和 TableLayout 這 5 種,而 iOS 的布局更少,只有 Frame 布局和自動布局兩種。
為了幫你建立起對布局類 Widget 的認知,了解基本布局類 Widget 的布局特點和用法,從而學以致用快速上手開發,在今天的這篇文章中,我特意挑選了幾類在開發 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 也不例外。'),
padding: EdgeInsets.all(18.0), // 內邊距
margin: EdgeInsets.all(44.0), // 外邊距
width: 180.0,
height:240,
alignment: Alignment.center, // 子 Widget 居中對齊
decoration: BoxDecoration( //Container 樣式
color: Colors.red, // 背景色
borderRadius: BorderRadius.circular(10.0), // 圓角邊框
),
)
~~~
:-: 
圖 1 Container 示例
如果我們只需要將子 Widget 設定間距,則可以使用另一個單子容器控件 Padding 進行內容填充:
~~~
Padding(
padding: EdgeInsets.all(44.0),
child: Text('Container(容器)在 UI 框架中是一個很常見的概念,Flutter 也不例外。'),
);
~~~
:-: 
圖 2 Padding 示例
在需要設置內容間距時,我們可以通過 EdgeInsets 的不同構造函數,分別制定四個方向的不同補白方式,如均使用同樣數值留白、只設置左留白或對稱方向留白等。如果你想更深入地了解這部分內容,可以參考這個[API 文檔](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html#constructors)。
接下來,我們再來看看單子 Widget 布局容器中另一個常用的容器 Center。正如它的名字一樣,Center 會將其子 Widget 居中排列。
比如,我們可以把一個 Text 包在 Center 里,實現居中展示:
~~~
Scaffold(
body: Center(child: Text("Hello")) // This trailing comma makes auto-formatting nicer for build methods.
);
~~~
:-: 
圖 3 Center 示例
需要注意的是,為了實現居中布局,Center 所占據的空間一定要比其子 Widget 要大才行,這也是顯而易見的:如果 Center 和其子 Widget 一樣大,自然就不需要居中,也沒空間居中了。因此 Center 通常會結合 Container 一起使用。
現在,我們結合 Container,一起看看 Center 的具體使用方法吧。
~~~
Container(
child: Center(child: Text('Container(容器)在 UI 框架中是一個很常見的概念,Flutter 也不例外。')),
padding: EdgeInsets.all(18.0), // 內邊距
margin: EdgeInsets.all(44.0), // 外邊距
width: 180.0,
height:240,
decoration: BoxDecoration( //Container 樣式
color: Colors.red, // 背景色
borderRadius: BorderRadius.circular(10.0), // 圓角邊框
),
);
~~~
可以看到,我們通過 Center 容器實現了 Container 容器中**alignment: Alignment.center**的效果。
事實上,為了達到這一效果,Container 容器與 Center 容器底層都依賴了同一個容器 Align,通過它實現子 Widget 的對齊方式。Align 的使用也比較簡單,如果你想深入了解的話,可以參考[官方文檔](https://api.flutter.dev/flutter/widgets/Align-class.html),這里我就不再過多介紹了。
接下來,我們再看看多子 Widget 布局的三種方式,即 Row、Column 與 Expanded。
## 多子 Widget 布局:Row、Column 與 Expanded
對于擁有多個子 Widget 的布局類容器而言,其布局行為無非就是兩種規則的抽象:水平方向上應該如何布局、垂直方向上應該如何布局。
如同 Android 的 LinearLayout、前端的 Flex 布局一樣,Flutter 中也有類似的概念,即將子 Widget 按行水平排列的 Row,按列垂直排列的 Column,以及負責分配這些子 Widget 在布局方向(行 / 列)中剩余空間的 Expanded。
Row 與 Column 的使用方法很簡單,我們只需要將各個子 Widget 按序加入到 chiildren 數組即可。在下面的代碼中,我們把 4 個分別設置了不同的顏色和寬高的 Container 加到 Row 與 Column 中:
~~~
//Row 的用法示范
Row(
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
);
//Column 的用法示范
Column(
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
);
~~~
:-: 
(a)Row 示例
:-: 
(b)Column 示例
圖 4 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.yellow, height: 60)), // 設置了 flex=1,因此寬度由 Expanded 來分配
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/ 設置了 flex=1,因此寬度由 Expanded 來分配
],
);
~~~
:-: 
圖 5 Expanded 控件示例
于 Row 與 Column 而言,Flutter 提供了依據坐標軸的布局對齊行為,即根據布局方向劃分出主軸和縱軸:主軸,表示容器依次擺放子 Widget 的方向;縱軸,則是與主軸垂直的另一個方向。
:-: 
圖 6 Row 和 Column 控件的主軸與縱軸
我們可以根據主軸與縱軸,設置子 Widget 在這兩個方向上的對齊規則 mainAxisAlignment 與 crossAxisAlignment。比如,主軸方向 start 表示靠左對齊、center 表示橫向居中對齊、end 表示靠右對齊、spaceEvenly 表示按固定間距對齊;而縱軸方向 start 則表示靠上對齊、center 表示縱向居中對齊、end 表示靠下對齊。
下圖展示了在 Row 中設置不同方向的對齊規則后的呈現效果:
:-: 
圖 7 Row 的主軸對齊方式
:-: 
圖 8 Row 的縱軸對齊方式
Column 的對齊方式也是類似的,我就不再過多展開了。
這里需要注意的是,對于主軸而言,Flutter 默認是讓父容器決定其長度,即盡可能大,類似 Android 中的 match\_parent。
在上面的例子中,Row 的寬度為屏幕寬度,Column 的高度為屏幕高度。主軸長度大于所有子 Widget 的總長度,意味著容器在主軸方向的空間比子 Widget 要大,這也是我們能通過主軸對齊方式設置子 Widget 布局效果的原因。
如果想讓容器與子 Widget 在主軸上完全匹配,我們可以通過設置 Row 的 mainAxisSize 參數為 MainAxisSize.min,由所有子 Widget 來決定主軸方向的容器長度,即主軸方向的長度盡可能小,類似 Android 中的 wrap\_content:
~~~
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 由于容器與子 Widget 一樣寬,因此這行設置排列間距的代碼并未起作用
mainAxisSize: MainAxisSize.min, // 讓容器寬度與所有子 Widget 的寬度一致
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
)
~~~
:-: 
圖 9 Row 的主軸大小
可以看到,我們設置了主軸大小為 MainAxisSize.min 之后,Row 的寬度變得和其子 Widget 一樣大,因此再設置主軸的對齊方式也就不起作用了。
## 層疊 Widget 布局:Stack 與 Positioned
有些時候,我們需要讓一個控件疊加在另一個控件的上面,比如在一張圖片上放置一段文字,又或者是在圖片的某個區域放置一個按鈕。這時候,我們就需要用到層疊布局容器 Stack 了。
Stack 容器與前端中的絕對定位、Android 中的 Frame 布局非常類似,子 Widget 之間允許疊加,還可以根據父容器上、下、左、右四個角的位置來確定自己的位置。
**Stack 提供了層疊布局的容器,而 Positioned 則提供了設置子 Widget 位置的能力**。接下來,我們就通過一個例子來看一下 Stack 和 Positioned 的具體用法吧。
在這個例子中,我先在 Stack 中放置了一塊 300*300 的黃色畫布,隨后在 (18,18) 處放置了一個 50*50 的綠色控件,然后在 (18,70) 處放置了一個文本控件。
~~~
Stack(
children: <Widget>[
Container(color: Colors.yellow, width: 300, height: 300),// 黃色容器
Positioned(
left: 18.0,
top: 18.0,
child: Container(color: Colors.green, width: 50, height: 50),// 疊加在黃色容器之上的綠色控件
),
Positioned(
left: 18.0,
top:70.0,
child: Text("Stack 提供了層疊布局的容器 "),// 疊加在黃色容器之上的文本
)
],
)
~~~
試著運行一下,可以看到,這三個子 Widget 都按照我們預定的規則疊加在一起了。
:-: 
圖 10 Stack 與 Positioned 容器示例
Stack 控件允許其子 Widget 按照創建的先后順序進行層疊擺放,而 Positioned 控件則用來控制這些子 Widget 的擺放位置。需要注意的是,Positioned 控件只能在 Stack 中使用,在其他容器中使用會報錯。
## 總結
Flutter 的布局容器強大而豐富,可以將小型、單用途的基本視覺元素快速封裝成控件。今天我選取了 Flutter 中最具代表性,也最常用的幾類布局 Widget,與你介紹了構建一個界面精美的 App 所需要的布局概念。
接下來,我們簡單回顧一下今天的內容,以便加深理解與記憶:
首先,我們認識了單子容器 Container、Padding 與 Center。其中,Container 內部提供了間距、背景樣式等基礎屬性,為子 Widget 的擺放方式,及展現樣式都提供了定制能力。而 Padding 與 Center 提供的功能,則正如其名一樣簡潔,就是對齊與居中。
然后,我們深入學習了多子 Widget 布局中的 Row 和 Column,各子 Widget 間對齊的規則,以及容器自身擴充的規則,以及如何通過 Expanded 控件使用容器內部的剩余空間,
最后,我們學習了層疊布局 Stack,以及與之搭配使用的,定位子 Widget 位置的 Positioned 容器,你可以通過它們,實現多個控件堆放的布局效果。
通過今天的文章,相信你已經對如何搭建 App 的界面有了足夠的知識儲備,所以在下一篇文章中,我會通過一些實際的例子,帶你認識在 Flutter 中,如何通過這些基本控件與布局規則,實現好看的界面。
## 思考題
最后,我給你留下一道思考題吧。
Row 與 Column 自身的大小是如何決定的?當它們嵌套時,又會出現怎樣的情況呢?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略