Flutter 中的佈局
瞭解 Flutter 的佈局機制以及如何構建應用的佈局。
概述
#Flutter 佈局機制的核心是 Widget。在 Flutter 中,幾乎一切都是 Widget——甚至是佈局模型本身也是 Widget。你在 Flutter 應用中看到的影像、圖示和文字都是 Widget。但那些你看不到的東西也是 Widget,例如用於排列、約束和對齊可見 Widget 的行(Row)、列(Column)和網格(Grid)。你透過組合 Widget 來構建更復雜的 Widget,從而建立佈局。
概念示例
#在下面的示例中,第一張截圖顯示了三個帶有標籤的圖示,第二張截圖顯示了行和列的視覺佈局。在第二張截圖中,debugPaintSizeEnabled 被設定為 true,以便你可以看到視覺佈局。
這是上述示例的 Widget 樹圖示
大部分內容應該如你所料,但你可能對其中的容器(以粉色顯示)感到好奇。Container 是一個 Widget 類,允許你自定義其子 Widget。當你想要新增內邊距、外邊距、邊框或背景顏色時,可以使用 Container,這些只是它功能的一部分。
每個 Text Widget 都被放置在一個 Container 中以新增外邊距。整個 Row 也被放置在一個 Container 中,以在行周圍新增內邊距。
其餘 UI 由屬性控制。使用 Icon 的 color 屬性設定其顏色。使用 Text.style 屬性來設定字型、顏色、字重等。列(Column)和行(Row)擁有屬性,允許你指定其子項如何進行垂直或水平對齊,以及子項應占用多少空間。
佈局一個 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;它提供了預設橫幅、背景顏色,並擁有用於新增抽屜(drawer)、小吃欄(snack bar)和底部工作表(bottom sheet)的 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 新增到 scaffold 的navigationBar屬性中。你可以使用CupertinoColors提供的顏色來配置你的 Widget,以匹配 iOS 設計。 -
要佈局應用的主體,請將 scaffold 的
child屬性設定為所需的 Widget,例如Center或Column。
要了解還可以新增哪些 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 樹嵌套了行和列。
你將在 巢狀 Row 和 Column 中實現 Pavlova 的部分佈局程式碼。
對齊 Widget
#你可以使用 mainAxisAlignment 和 crossAxisAlignment 屬性控制行或列如何對齊其子項。對於行,主軸是水平的,交叉軸是垂直的。對於列,主軸是垂直的,交叉軸是水平的。
MainAxisAlignment 和 CrossAxisAlignment 列舉提供了多種用於控制對齊的常量。
在以下示例中,3 個影像中的每一個寬度均為 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 Widget 調整 Widget 大小以適應行或列。要修復前一個影像行對於渲染框來說太寬的示例,請使用 Expanded Widget 包裝每個影像。
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
也許你希望一個 Widget 佔據其兄弟節點兩倍的空間。為此,請使用 Expanded Widget 的 flex 屬性,這是一個整數,決定了 Widget 的彈性因子。預設彈性因子為 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
巢狀 Row 和 Column
#佈局框架允許你根據需要深度巢狀行和列。讓我們看看以下佈局中突出顯示部分的相應程式碼:
突出顯示的部分實現為兩行。評分行包含五顆星和評論數量。圖示行包含三列圖示和文字。
評分行的 Widget 樹:
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 列;每一列都包含一個圖示和兩行文字,正如你在其 Widget 樹中看到的那樣:
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 擁有豐富的佈局 Widget 庫。以下是一些最常用的 Widget。我們的目的是讓你儘快上手,而不是用完整的列表淹沒你。有關其他可用 Widget 的資訊,請參閱 Widget 目錄,或在 API 參考文件 中使用搜索框。此外,API 文件中的 Widget 頁面通常會建議可能更適合你需求的其他類似 Widget。
以下 Widget 分為兩類:widgets 庫 中的標準 Widget,以及 Material 庫 中的專用 Widget。任何應用都可以使用 widgets 庫,但只有 Material 應用可以使用 Material 元件庫。
-
CupertinoPageScaffold 為 iOS 風格的頁面提供基本佈局結構。
-
CupertinoNavigationBar 在螢幕頂部建立 iOS 風格的導航欄。
-
CupertinoSegmentedControl 建立用於選擇的分段控制元件。
-
CupertinoTabBar和CupertinoTabScaffold 建立典型的 iOS 底部標籤欄。
容器
#許多佈局都會大量使用 Container 來透過內邊距分隔 Widget,或新增邊框和外邊距。你可以透過將整個佈局放入 Container 並更改其背景顏色或影像來更改裝置的背景。
總結 (Container)
#- 新增內邊距、外邊距、邊框
- 更改背景顏色或影像
- 包含單個子 Widget,但該子項可以是
Row、Column甚至是 Widget 樹的根節點。
示例 (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 將 Widget 佈局為二維列表。GridView 提供了兩個預製列表,或者你可以構建自己的自定義網格。當 GridView 檢測到其內容太長而無法適應渲染框時,它會自動滾動。
總結 (GridView)
#- 以網格形式佈局 Widget
- 檢測列內容何時超過渲染框並自動提供滾動
- 構建你自己的自定義網格,或使用提供的網格之一
GridView.count允許你指定列數GridView.extent允許你指定瓦片(tile)的最大畫素寬度
示例 (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 是一種類似列的 Widget,當內容對於其渲染框來說太長時,它會自動提供滾動功能。
總結 (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 在基礎 Widget(通常是影像)之上排列 Widget。這些 Widget 可以完全或部分重疊基礎 Widget。
總結 (Stack)
#- 用於與其他 Widget 重疊的 Widget
- 子項列表中的第一個 Widget 是基礎 Widget;後續子項將覆蓋在該基礎 Widget 之上
Stack的內容無法滾動- 你可以選擇剪裁超出渲染框的子項
示例 (Stack)
#
使用 Stack 將 Container(在半透明黑色背景上顯示其 Text)覆蓋在 CircleAvatar 之上。Stack 使用 alignment 屬性和 Alignments 來偏移文字。
應用原始碼: 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
#來自 Material 庫 的 Card 包含相關的資訊片段,幾乎可以由任何 Widget 組成,但通常與 ListTile 一起使用。Card 只有一個子項,但該子項可以是列、行、列表、網格或其他支援多個子項的 Widget。預設情況下,Card 會將其大小縮小到 0x0 畫素。你可以使用 SizedBox 來約束卡片的大小。
在 Flutter 中,Card 具有略微圓角和投影,使其具有 3D 效果。更改 Card 的 elevation 屬性允許你控制投影效果。例如,將高程(elevation)設定為 24,視覺上會將 Card 從表面進一步提升,並使陰影變得更加分散。有關支援的高程值列表,請參閱 Material 指南 中的 Elevation。指定不支援的值會完全停用投影。
總結 (Card)
#- 實現 Material 卡片
- 用於呈現相關的資訊片段
- 接受單個子項,但該子項可以是
Row、Column或其他包含子項列表的 Widget - 以圓角和投影顯示
Card的內容無法滾動- 來自 Material 庫
示例 (Card)
#
一個包含 3 個 ListTile 的 Card,並透過使用 SizedBox 包裝來調整大小。Divider 將第一個和第二個 ListTile 分隔開。
應用原始碼: 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 庫 的專用行 Widget)可以輕鬆建立一個包含最多 3 行文字以及可選前導和尾隨圖示的行。ListTile 最常用於 Card 或 ListView 中,但也可用於其他地方。
總結 (ListTile)
#- 一種專用行,包含最多 3 行文字和可選圖示
- 比
Row配置選項少,但更易於使用 - 來自 Material 庫
示例 (ListTile)
#約束
#要完全理解 Flutter 的佈局系統,你需要了解 Flutter 如何在佈局中定位和調整元件的大小。有關更多資訊,請參閱 理解約束。
影片
#以下影片是 Flutter in Focus 系列的一部分,解釋了 Stateless 和 Stateful Widget。
Widget of the Week 系列 的每一集都專注於一個 Widget。其中幾個集數包含了佈局 Widget。
Flutter Widget of the Week 播放列表
其他資源
#以下資源在編寫佈局程式碼時可能會有所幫助。
- 佈局教程
學習如何構建佈局。
- 元件目錄
描述了 Flutter 中可用的許多 Widget。
- Flutter 中的 HTML/CSS 對應物
-
對於熟悉 Web 程式設計的人來說,此頁面將 HTML/CSS 功能對映到 Flutter 功能。
- API 參考文件
所有 Flutter 庫的參考文件。
- 新增資源和影像
解釋如何嚮應用包中新增影像和其他資源。
- 從零到一使用 Flutter
一個人編寫其第一個 Flutter 應用的經驗。





