Flutter 中的佈局
瞭解 Flutter 的佈局機制以及如何構建你的應用的佈局。
概述
#Flutter 佈局機制的核心是 Widget。在 Flutter 中,幾乎所有東西都是 Widget——甚至佈局模型也是 Widget。你在 Flutter 應用中看到的所有影像、圖示和文字都是 Widget。但你看不到的東西也是 Widget,例如排列、約束和對齊可見 Widget 的行、列和網格。你透過組合 Widget 來構建更復雜的 Widget,從而建立佈局。
概念示例
#在下面的示例中,第一張截圖顯示了帶有標籤的三個圖示,第二張截圖包含行和列的視覺佈局。在第二張截圖中,debugPaintSizeEnabled 設定為 true,以便你可以看到視覺佈局。
這是前一個示例的 Widget 樹的圖表
其中大部分應該如你所料,但你可能想知道容器(顯示為粉色)。 Container 是一個 Widget 類,允許你自定義其子 Widget。當你想要新增填充、邊距、邊框或背景顏色時,可以使用 Container,僅舉其幾項功能。
每個 Text Widget 放置在一個 Container 中以新增邊距。整個 Row 也放置在一個 Container 中,以在行周圍新增填充。
UI 的其餘部分由屬性控制。使用其 color 屬性設定 Icon 的顏色。使用 Text.style 屬性設定字型、顏色、粗細等。列和行具有屬性,允許你指定其子項在垂直或水平方向上如何對齊,以及子項應占用多少空間。
佈局一個 Widget
#如何在 Flutter 中佈局單個 Widget?本節向你展示瞭如何建立和顯示一個簡單的 Widget。它還顯示了一個簡單的 Hello World 應用的完整程式碼。
在 Flutter 中,只需幾個步驟就可以將文字、圖示或影像放在螢幕上。
1. 選擇一個佈局 Widget
#根據你希望如何對齊或約束可見的 Widget,從各種 佈局 Widget 中進行選擇,因為這些特性通常會傳遞給包含的 Widget。
例如,你可以使用 Center 佈局 Widget 水平和垂直居中一個可見的 Widget
Center(
// Content to be centered here.
)
2. 建立一個可見的 Widget
#選擇一個 可見的 Widget 用於你的應用,以包含可見元素,例如 文字、影像 或 圖示。
例如,你可以使用 Text Widget 顯示一些文字
Text('Hello World')
3. 將可見的 Widget 新增到佈局 Widget
#所有佈局 Widget 都有以下兩種之一
- 如果它們接受單個子項,則具有
child屬性——例如,Center或Container - 如果它們接受 Widget 列表,則具有
children屬性——例如,Row、Column、ListView或Stack。
將 Text Widget 新增到 Center Widget
const Center(
child: Text('Hello World'),
),
4. 將佈局 Widget 新增到頁面
#Flutter 應用本身就是一個 Widget,並且大多數 Widget 都有一個 build() 方法。在應用的 build() 方法中例項化並返回 Widget 會顯示該 Widget。
對於通用應用,你可以將 Container Widget 新增到應用的 build() 方法
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(color: Colors.white),
child: const Center(
child: Text(
'Hello World',
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 32, color: Colors.black87),
),
),
);
}
}
預設情況下,通用應用不包含 AppBar、標題或背景顏色。如果你希望在通用應用中擁有這些功能,則必須自己構建它們。此應用將背景顏色更改為白色,並將文字更改為深灰色,以模擬 Material 應用。
對於 Material 應用,你可以使用 Scaffold Widget;它提供預設的橫幅、背景顏色,並具有用於新增抽屜、小吃欄和底部表單的 API。然後,你可以將 Center Widget 直接新增到主頁的 body 屬性中。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const String appTitle = 'Flutter layout demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
appBar: AppBar(title: const Text(appTitle)),
body: const Center(
child: Text('Hello World'),
),
),
);
}
}
要建立一個 Cupertino 應用,請使用 CupertinoApp 和 CupertinoPageScaffold Widget。
與 Material 不同,它不提供預設的橫幅或背景顏色。你需要自己設定這些顏色。
-
要設定預設顏色,請將配置的
CupertinoThemeData傳遞到你的應用的theme屬性。 -
要在應用的頂部新增 iOS 樣式的導航欄,請將
CupertinoNavigationBarWidget 新增到支架的navigationBar屬性。你可以使用CupertinoColors提供的顏色來配置你的 Widget,以匹配 iOS 設計。 -
要佈局應用的 body,請使用所需的 Widget(例如
Center或Column)作為其值設定支架的child屬性。
要了解你可以新增的其他 UI 元件,請檢視 Cupertino 庫。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
title: 'Flutter layout demo',
theme: CupertinoThemeData(
brightness: Brightness.light,
primaryColor: CupertinoColors.systemBlue,
),
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
backgroundColor: CupertinoColors.systemGrey,
middle: Text('Flutter layout demo'),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('Hello World')],
),
),
),
);
}
}
5. 執行你的應用
#
垂直和水平佈局多個 Widget
#最常見的佈局模式之一是水平或垂直排列 Widget。你可以使用 Row Widget 水平排列 Widget,並使用 Column Widget 垂直排列 Widget。
要在 Flutter 中建立行或列,你將一個子 Widget 列表新增到 Row 或 Column Widget。反過來,每個子項本身可以是一個行或列,依此類推。以下示例顯示瞭如何在行或列內巢狀行或列。
此佈局組織為 Row。該行包含兩個子項:左側的列和右側的影像
左側列的 Widget 樹嵌套了行和列。
你將在 巢狀行和列 中實現 Pavlova 的一些佈局程式碼。
對齊 Widget
#你使用 mainAxisAlignment 和 crossAxisAlignment 屬性控制行或列如何對其子項進行對齊。對於行,主軸水平執行,交叉軸垂直執行。對於列,主軸垂直執行,交叉軸水平執行。
MainAxisAlignment 和 CrossAxisAlignment 列舉提供各種常量,用於控制對齊方式。
在以下示例中,每張影像的寬度均為 100 畫素。渲染框(在本例中為整個螢幕)的寬度大於 300 畫素,因此將主軸對齊方式設定為 spaceEvenly 會在每張影像之間、之前和之後均勻地分配剩餘的水平空間。
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),
Image.asset('images/pic2.jpg'),
Image.asset('images/pic3.jpg'),
],
);

應用原始碼: row_column
列的工作方式與行相同。以下示例顯示包含 3 張影像的列,每張影像的高度均為 100 畫素。渲染框的高度(在本例中為整個螢幕)大於 300 畫素,因此將主軸對齊方式設定為 spaceEvenly 會在每張影像之間、上方和下方均勻地分配剩餘的垂直空間。
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),
Image.asset('images/pic2.jpg'),
Image.asset('images/pic3.jpg'),
],
);

應用原始碼: row_column
調整 Widget 大小
#當佈局太大而無法適應裝置時,受影響的邊緣會出現黃色和黑色條紋圖案。這裡有一個 示例,展示了一個過寬的行。
可以使用 Expanded 部件將部件的大小調整為適應行或列。要修復上一個示例中行中的影像過寬的問題,請使用 Expanded 部件包裝每張影像。
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: Image.asset('images/pic1.jpg')),
Expanded(child: Image.asset('images/pic2.jpg')),
Expanded(child: Image.asset('images/pic3.jpg')),
],
);

應用原始碼: sizing
也許您希望某個部件佔據與其兄弟部件兩倍的空間。為此,請使用 Expanded 部件的 flex 屬性,該屬性是一個整數,用於確定部件的彈性因子。預設彈性因子為 1。以下程式碼將中間影像的彈性因子設定為 2。
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: Image.asset('images/pic1.jpg')),
Expanded(flex: 2, child: Image.asset('images/pic2.jpg')),
Expanded(child: Image.asset('images/pic3.jpg')),
],
);

應用原始碼: sizing
打包 Widget
#預設情況下,行或列會盡可能多地佔據主軸上的空間,但如果您希望將子部件緊密地排列在一起,請將其 mainAxisSize 設定為 MainAxisSize.min。以下示例使用此屬性將星形圖示打包在一起。
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
const Icon(Icons.star, color: Colors.black),
const Icon(Icons.star, color: Colors.black),
],
)

應用原始碼: pavlova
巢狀行和列
#佈局框架允許您根據需要將行和列巢狀在行和列中。讓我們檢視以下佈局中輪廓部分的的程式碼。
輪廓部分由兩行實現。評分行包含五個星級和一個評論數。圖示行包含三列圖示和文字。
評分行的部件樹
ratings 變數建立一個包含較小的一行 5 星圖示和文字的行。
final stars = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
const Icon(Icons.star, color: Colors.black),
const Icon(Icons.star, color: Colors.black),
],
);
final ratings = Container(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
stars,
const Text(
'170 Reviews',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w800,
fontFamily: 'Roboto',
letterSpacing: 0.5,
fontSize: 20,
),
),
],
),
);
位於評分行下方的圖示行包含 3 列;每列包含一個圖示和兩行文字,如其部件樹所示。
iconList 變數定義圖示行。
const descTextStyle = TextStyle(
color: Colors.black,
fontWeight: FontWeight.w800,
fontFamily: 'Roboto',
letterSpacing: 0.5,
fontSize: 18,
height: 2,
);
// DefaultTextStyle.merge() allows you to create a default text
// style that is inherited by its child and all subsequent children.
final iconList = DefaultTextStyle.merge(
style: descTextStyle,
child: Container(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Icon(Icons.kitchen, color: Colors.green[500]),
const Text('PREP:'),
const Text('25 min'),
],
),
Column(
children: [
Icon(Icons.timer, color: Colors.green[500]),
const Text('COOK:'),
const Text('1 hr'),
],
),
Column(
children: [
Icon(Icons.restaurant, color: Colors.green[500]),
const Text('FEEDS:'),
const Text('4-6'),
],
),
],
),
),
);
leftColumn 變數包含評分行和圖示行,以及描述 Pavlova 的標題和文字。
final leftColumn = Container(
padding: const EdgeInsets.fromLTRB(20, 30, 20, 20),
child: Column(children: [titleText, subTitle, ratings, iconList]),
);
左列放置在 SizedBox 中以限制其寬度。最後,UI 使用整個行(包含左列和影像)在 Card 中構建。
Pavlova 影像 來自 Pixabay。您可以使用 Image.network() 嵌入來自網路的影像,但對於此示例,影像已儲存到專案的 images 目錄中,新增到 pubspec 檔案 中,並使用 Images.asset() 訪問。有關更多資訊,請參閱 新增資源和影像。
body: Center(
child: Container(
margin: const EdgeInsets.fromLTRB(0, 40, 0, 30),
height: 600,
child: Card(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 440, child: leftColumn),
mainImage,
],
),
),
),
),
應用原始碼: pavlova
常用的佈局 Widget
#Flutter 具有豐富的佈局部件庫。以下是一些最常用的部件。目的是讓您儘快上手,而不是讓您不堪重負地瞭解完整的列表。有關其他可用部件的資訊,請參閱 部件目錄,或在 API 參考文件 中使用搜索框。此外,API 文件中的部件頁面通常會提出有關可能更適合您需求的類似部件的建議。
以下部件分為兩類:來自 widgets 庫 的標準部件,以及來自 Material 庫 的專用部件。任何應用程式都可以使用 widgets 庫,但只有 Material 應用程式才能使用 Material Components 庫。
-
CupertinoPageScaffold 為 iOS 風格的頁面提供基本的佈局結構。
-
CupertinoNavigationBar 在螢幕頂部建立一個 iOS 風格的導航欄。
-
CupertinoSegmentedControl 建立一個用於選擇的分段控制元件。
-
CupertinoTabBar和CupertinoTabScaffold 建立典型的 iOS 底部的選項卡欄。
容器
#許多佈局會大量使用 Container 來使用填充分隔部件,或新增邊框或邊距。您可以透過將整個佈局放置在 Container 中並更改其背景色或影像來更改裝置的背景。
摘要 (Container)
#- 新增填充、邊距、邊框
- 更改背景色或影像
- 包含單個子部件,但該子部件可以是
Row、Column,甚至是部件樹的根。
示例 (Container)
#此佈局由一個包含兩行、每行包含 2 張影像的列組成。 Container 用於將列的背景色更改為較淺的灰色。
Widget _buildImageColumn() {
return Container(
decoration: const BoxDecoration(color: Colors.black26),
child: Column(children: [_buildImageRow(1), _buildImageRow(3)]),
);
}
Container 還用於為每張影像新增圓角邊框和邊距
Widget _buildDecoratedImage(int imageIndex) => Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 10, color: Colors.black38),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
margin: const EdgeInsets.all(4),
child: Image.asset('images/pic$imageIndex.jpg'),
),
);
Widget _buildImageRow(int imageIndex) => Row(
children: [
_buildDecoratedImage(imageIndex),
_buildDecoratedImage(imageIndex + 1),
],
);
您可以在 教程 中找到更多 Container 示例。
應用原始碼: container
GridView
#使用 GridView 將部件佈局為二維列表。GridView 提供兩個預製列表,或者您可以構建自己的自定義網格。當 GridView 檢測到其內容太大而無法適應渲染框時,它會自動滾動。
摘要 (GridView)
#- 將部件佈局為網格
- 檢測到列內容超過渲染框並自動提供滾動
- 構建自己的自定義網格,或使用提供的網格之一
GridView.count允許您指定列數GridView.extent允許您指定瓷片的最大畫素寬度
示例 (GridView)
#
使用 GridView.count 建立一個在縱向模式下為 2 列,在橫向模式下為 3 列的網格。標題是透過為每個 GridTile 設定 footer 屬性建立的。
Dart 程式碼: grid_list_demo.dart
Widget _buildGrid() => GridView.extent(
maxCrossAxisExtent: 150,
padding: const EdgeInsets.all(4),
mainAxisSpacing: 4,
crossAxisSpacing: 4,
children: _buildGridTileList(30),
);
// The images are saved with names pic0.jpg, pic1.jpg...pic29.jpg.
// The List.generate() constructor allows an easy way to create
// a list when objects have a predictable naming pattern.
List<Widget> _buildGridTileList(int count) =>
List.generate(count, (i) => Image.asset('images/pic$i.jpg'));
ListView
#
ListView 是一種類似於列的部件,當其內容太大而無法適應其渲染框時,會自動提供滾動。
摘要 (ListView)
#- 一個專門的
Column,用於組織一個盒子列表 - 可以水平或垂直佈局
- 檢測到其內容不合適並提供滾動
- 比
Column的可配置性低,但更易於使用並支援滾動
示例 (ListView)
#Widget _buildList() {
return ListView(
children: [
_tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters),
_tile('The Castro Theater', '429 Castro St', Icons.theaters),
_tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters),
_tile('Roxie Theater', '3117 16th St', Icons.theaters),
_tile(
'United Artists Stonestown Twin',
'501 Buckingham Way',
Icons.theaters,
),
_tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters),
const Divider(),
_tile('K\'s Kitchen', '757 Monterey Blvd', Icons.restaurant),
_tile('Emmy\'s Restaurant', '1923 Ocean Ave', Icons.restaurant),
_tile('Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant),
_tile('La Ciccia', '291 30th St', Icons.restaurant),
],
);
}
ListTile _tile(String title, String subtitle, IconData icon) {
return ListTile(
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
subtitle: Text(subtitle),
leading: Icon(icon, color: Colors.blue[500]),
);
}
層疊佈局
#使用 Stack 將部件排列在基本部件之上,通常是影像。部件可以完全或部分覆蓋基本部件。
摘要 (Stack)
#- 用於與其他部件重疊的部件
- 列表中的第一個部件是基本部件;後續部件覆蓋在該基本部件之上
Stack的內容無法滾動- 您可以選擇剪裁超出渲染框的子部件
示例 (Stack)
#
使用 Stack 將一個 Container(在其 Text 上顯示半透明黑色背景)疊加在 CircleAvatar 之上。Stack 使用 alignment 屬性和 Alignment 對文字進行偏移。
應用原始碼: card_and_stack
Widget _buildStack() {
return Stack(
alignment: const Alignment(0.6, 0.6),
children: [
const CircleAvatar(
backgroundImage: AssetImage('images/pic.jpg'),
radius: 100,
),
Container(
decoration: const BoxDecoration(color: Colors.black45),
child: const Text(
'Mia B',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
);
}
Card
#一個 Card,來自 Material 庫,包含相關的資訊片段,並且可以由幾乎任何小部件組成,但通常與 ListTile 一起使用。Card 只有一個子元件,但其子元件可以是一個列、行、列表、網格或其他支援多個子元件的小部件。預設情況下,Card 的大小縮小到 0x0 畫素。您可以使用 SizedBox 來約束卡片的大小。
在 Flutter 中,Card 具有略微圓角的邊框和陰影,使其具有 3D 效果。更改 Card 的 elevation 屬性可以控制陰影效果。例如,將 elevation 設定為 24,可以使 Card 在視覺上遠離表面,並使陰影更加分散。有關支援的 elevation 值列表,請參閱 Elevation 在 Material 指南 中。
摘要 (Card)
#- 實現一個 Material 卡片
- 用於呈現相關的資訊片段
- 接受一個子元件,但該子元件可以是
Row、Column或其他包含子元件列表的小部件 - 以圓角和陰影顯示
Card的內容無法滾動- 來自 Material 庫
示例 (Card)
#
一個包含 3 個 ListTile 的 Card,並透過將其包裝在 SizedBox 中來確定其大小。一個 Divider 分隔了第一個和第二個 ListTiles。
應用原始碼: card_and_stack
Widget _buildCard() {
return SizedBox(
height: 210,
child: Card(
child: Column(
children: [
ListTile(
title: const Text(
'1625 Main Street',
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: const Text('My City, CA 99984'),
leading: Icon(Icons.restaurant_menu, color: Colors.blue[500]),
),
const Divider(),
ListTile(
title: const Text(
'(408) 555-1212',
style: TextStyle(fontWeight: FontWeight.w500),
),
leading: Icon(Icons.contact_phone, color: Colors.blue[500]),
),
ListTile(
title: const Text('costa@example.com'),
leading: Icon(Icons.contact_mail, color: Colors.blue[500]),
),
],
),
),
);
}
ListTile
#使用 ListTile,來自 Material 庫 的一個專門的行小部件,可以輕鬆建立包含最多 3 行文字和可選的引導和尾隨圖示的行。ListTile 最常用於 Card 或 ListView 中,但也可以在其他地方使用。
摘要 (ListTile)
#- 一個包含最多 3 行文字和可選圖示的專用行
- 配置不如
Row靈活,但更易於使用 - 來自 Material 庫
示例 (ListTile)
#約束
#要完全理解 Flutter 的佈局系統,您需要了解 Flutter 如何定位和調整佈局中的元件的大小。有關更多資訊,請參閱 瞭解約束。
影片
#以下影片是 Flutter in Focus 系列的一部分,解釋了 Stateless 和 Stateful 小部件。
《Widget of the Week 系列》的每個劇集都專注於一個小部件。其中一些包括佈局小部件。
Flutter Widget of the Week 播放列表
其他資源
#以下資源可能在編寫佈局程式碼時有所幫助。
- 佈局教程
學習如何構建佈局。
- 元件目錄
描述了 Flutter 中可用的許多小部件。
- Flutter 中的 HTML/CSS 類比
-
對於熟悉 Web 程式設計的人員,此頁面將 HTML/CSS 功能對映到 Flutter 功能。
- API 參考文件
Flutter 所有庫的參考文件。
- 新增資源和影像
解釋瞭如何將影像和其他資源新增到應用程式的包中。
- Flutter 從零到一
一個人編寫他們的第一個 Flutter 應用程式的經驗。





