佈局建構課程
這是一份如何在 Flutter 中建構佈局的指南。你將為如下 app 建立佈局:

這份指南之前溯源一步解釋了 Flutter 中的佈局方式,以及展示瞭如何在螢幕中放置單個 widget。經過了如何水平以及豎直放置 widgets 的討論之後,一些最常使用的 widgets 都涉及到了。
如果你想對佈局機制有個”全域”的理解,可以先從 Flutter 中的佈局 開始.
第一步: 建立 app 基礎程式碼
確保你已經 安裝和配置 好了你的環境,然後做如下步驟:
-
用下面的程式碼來替換你的
lib/main.dart
檔案:lib/main.dart (all)import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter layout demo', home: Scaffold( appBar: AppBar( title: const Text('Flutter layout demo'), ), body: const Center( child: Text('Hello World'), ), ), ); } }
第一步: 對佈局進行圖形分解
第一步需要將佈局分解成它的各個基礎元素:
-
識別出它的行和列。
-
這個佈局是否包含網格佈局?
-
是否有重疊的元素?
-
介面是否需要選項卡?
-
留意需要對齊、內間距、或者邊界的區域。
首先,識別出稍大的元素。在這個例子中,四個元素排成一列:一個影象,兩個行區域,和一個文字區域。

接著,對每一行進行圖解。第一行,也就是標題區域,有三個子元素:一個文字列,一個星形圖示,和一個數字。它的第一個子元素,文字列,包含兩行文字。第一列佔據大量空間,因此它應當被封裝在一個 Expanded widget 當中。

第二行,也就是按鈕區域,同樣有三個子元素:每個子元素是一個包含圖示和文字的列。

一旦圖解好佈局,採取自下而上的方法來實現它就變得尤為輕鬆了。為了最大程度減少,深層巢狀(Nesting)的佈局程式碼帶來的視覺混亂,需要用一些變數和函式來替代某些實現。
第二步: 實現標題行
首先,你可以建構標題部分左側列。新增如下程式碼到 MyApp
類別的 build()
方法內頂部。
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Container(
padding: const EdgeInsets.only(bottom: 8),
child: const Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
/*3*/
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
],
),
);
-
將 Column 元素放到 Expanded widget 中可以拉伸該列,以利用該行中所有剩餘的閒置空間。設定
crossAxisAlignment
屬性值為CrossAxisAlignment.start
,這會將該列放置在行的起始位置。 -
將第一行文字放入 Container 容器中使得你可以增加內間距。列中的第二個子元素,同樣為文字,顯示為灰色。
-
標題行中的最後兩項是一個紅色星形圖示,和文字”41”。整行都在一個 Container 容器佈局中,而且每條邊都有 32 畫素的內間距。
如下新增標題部分到 app body 中:
@@ -14,11 +48,13 @@
|
|
14
48
|
return MaterialApp(
|
15
49
|
title: 'Flutter layout demo',
|
16
50
|
home: Scaffold(
|
17
51
|
appBar: AppBar(
|
18
52
|
title: const Text('Flutter layout demo'),
|
19
53
|
),
|
20
|
-
body:
|
21
|
-
|
54
|
+
body: Column(
|
55
|
+
children: [
|
56
|
+
titleSection,
|
57
|
+
],
|
22
58
|
),
|
23
59
|
),
|
24
60
|
);
|
第三步: 實現按鈕行
按鈕區域包含三列使用相同佈局-一行文字上面一個圖示。此行的各列被等間隙放置,文字和圖示被著以初始色。
由於建構每列的程式碼基本相同,因此可以建立一個名為
buildButtonColumn()
的私有輔助函式,以顏色、圖示和文字為入參,返回一個以指定顏色繪製自身
widgets 的一個 column 列物件。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// ···
}
Column _buildButtonColumn(Color color, IconData icon, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
}
這個函式直接將圖示新增到這列裡。文字在以一個僅有上間距的 Container 容器中,使得文字與圖示分隔開。
透過呼叫函式並傳遞針對某列的顏色,Icon
圖示和文字,來建構包含這些列的行。然後在行的主軸方向透過使用 MainAxisAlignment.spaceEvenly
,將剩餘的空間均分到每列各自的前後及中間。只需在 build()
方法中的 titleSection
宣告下新增如下程式碼:
Color color = Theme.of(context).primaryColor;
Widget buttonSection = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildButtonColumn(color, Icons.call, 'CALL'),
_buildButtonColumn(color, Icons.near_me, 'ROUTE'),
_buildButtonColumn(color, Icons.share, 'SHARE'),
],
);
新增按鈕部分到 body 屬性中去:
@@ -48,3 +59,3 @@
|
|
48
59
|
return MaterialApp(
|
49
60
|
title: 'Flutter layout demo',
|
50
61
|
home: Scaffold(
|
@@ -54,8 +65,9 @@
|
|
54
65
|
body: Column(
|
55
66
|
children: [
|
56
67
|
titleSection,
|
68
|
+
buttonSection,
|
57
69
|
],
|
58
70
|
),
|
59
71
|
),
|
60
72
|
);
|
61
73
|
}
|
第四步: 實現文字區域
將文字區域定義為一個變數,將文字放置到一個 Container 容器中,然後為每條邊新增內邊距。只需在 buttonSection
宣告下新增如下程式碼:
Widget textSection = const Padding(
padding: EdgeInsets.all(32),
child: Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
'Alps. Situated 1,578 meters above sea level, it is one of the '
'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
'half-hour walk through pastures and pine forest, leads you to the '
'lake, which warms to 20 degrees Celsius in the summer. Activities '
'enjoyed here include rowing, and riding the summer toboggan run.',
softWrap: true,
),
);
透過設定 softwrap
為 true,文字將在填充滿列寬後在單詞邊界處自動換行。
新增文字部分到 body 屬性:
@@ -59,3 +72,3 @@
|
|
59
72
|
return MaterialApp(
|
60
73
|
title: 'Flutter layout demo',
|
61
74
|
home: Scaffold(
|
@@ -66,6 +79,7 @@
|
|
66
79
|
children: [
|
67
80
|
titleSection,
|
68
81
|
buttonSection,
|
82
|
+
textSection,
|
69
83
|
],
|
70
84
|
),
|
71
85
|
),
|
第五步: 實現圖片區域
四個列元素中的三個已經完成了,只剩下圖片部分了。如下新增圖片檔案到範例工程中:
- Create an
images
directory at the top of the project. -
新增
lake.jpg
-
更新
pubspec.yaml
檔案,新增一個assets
標籤。這使得在你的程式碼中可以存取到該圖片。{step4 → step5}/pubspec.yaml@@ -18,3 +18,5 @@1818flutter:1919uses-material-design: true20+ assets:21+ - images/lake.jpg
現在你可以在你的程式碼中參考該圖片了:
@@ -77,6 +77,12 @@
|
|
77
77
|
),
|
78
78
|
body: Column(
|
79
79
|
children: [
|
80
|
+
Image.asset(
|
81
|
+
'images/lake.jpg',
|
82
|
+
width: 600,
|
83
|
+
height: 240,
|
84
|
+
fit: BoxFit.cover,
|
85
|
+
),
|
80
86
|
titleSection,
|
81
87
|
buttonSection,
|
82
88
|
textSection,
|
BoxFit.cover
告訴系統圖片應當儘可能等比縮小到剛好能夠覆蓋住整個渲染 box。
第六步: 最終的收尾
在最後的步驟中,需要在一個 ListView
中排列好所有的元素,而不是在一個 Column
中,因為當 app 執行在某個小裝置上時,ListView
支援 app body 的滾動。
@@ -72,13 +77,13 @@
|
|
72
77
|
return MaterialApp(
|
73
78
|
title: 'Flutter layout demo',
|
74
79
|
home: Scaffold(
|
75
80
|
appBar: AppBar(
|
76
81
|
title: const Text('Flutter layout demo'),
|
77
82
|
),
|
78
|
-
body:
|
83
|
+
body: ListView(
|
79
84
|
children: [
|
80
85
|
Image.asset(
|
81
86
|
'images/lake.jpg',
|
82
87
|
width: 600,
|
83
88
|
height: 240,
|
84
89
|
fit: BoxFit.cover,
|
Dart code: main.dart
Image: images
Pubspec: pubspec.yaml
大功告成!當你熱載入 app 時,你應當可以看到和本頁開頭截圖一樣的 app 佈局了。
你可以參考文件 為你的 Flutter 應用加入互動體驗 來給這個佈局增加互動。