構建 Flutter 佈局
學習如何在 Flutter 中構建佈局。
本教程介紹如何在 Flutter 中設計和構建佈局。
如果您使用提供的示例程式碼,您可以構建出以下應用。
最終成品應用。
攝影:Dino Reichmuth (來自 Unsplash)。文字:瑞士旅遊局。
若要更全面地瞭解佈局機制,請先從 Flutter 的佈局方案開始。
繪製佈局草圖
#在這一部分,請思考您想要為應用使用者提供什麼樣的使用者體驗。
思考如何定位使用者介面的元件。佈局即這些定位的總和。考慮提前規劃佈局以加快編碼速度。使用視覺線索來確定螢幕元素的位置會有很大幫助。
無論您喜歡哪種方式(例如介面設計工具或紙筆),請在編寫程式碼之前先確定要在螢幕上放置元素的位置。這就是程式設計版的諺語:“三思而後行”。
問自己以下問題,將佈局拆解為基礎元素。
- 你能識別出哪些是行,哪些是列嗎?
- 佈局是否包含網格?
- 是否有重疊的元素?
- UI 是否需要標籤頁(tabs)?
- 你需要對齊、新增內邊距(padding)或邊框的地方在哪裡?
識別出較大的元素。在這個示例中,您將影像、標題、按鈕和描述排列成一個列(Column)。
佈局中的主要元素:影像、行、行和文字塊
為每一行繪製圖表。
第 1 行(標題部分)有三個子元素:一個文字列、一個星形圖示和一個數字。它的第一個子元素(列)包含兩行文字。第一列可能需要更多空間。
帶有文字塊和圖示的標題部分
第 2 行(按鈕部分)有三個子元素:每個子元素包含一個列,該列中又包含一個圖示和一段文字。
帶有三個標註按鈕的按鈕部分
規劃好佈局後,思考如何編寫程式碼。
你會把所有程式碼寫在一個類裡嗎?還是會為佈局的每個部分建立一個類?
遵循 Flutter 的最佳實踐,為佈局的每個部分建立一個類(或元件)。當 Flutter 需要重新渲染 UI 的一部分時,它會更新變化的最微小部分。這就是為什麼 Flutter “一切皆元件”。如果 Text 元件中只有文字改變了,Flutter 就只會重繪那部分文字。Flutter 會在響應使用者輸入時儘可能少地更改 UI。
在本教程中,請將您識別出的每個元素編寫為獨立的元件。
建立應用基礎程式碼
#在這一部分,編寫 Flutter 應用的基礎程式碼以啟動應用。
-
用以下程式碼替換
lib/main.dart的內容。此應用使用引數來設定應用標題,並顯示在應用的appBar中。這種方式簡化了程式碼。dartimport 'package:flutter/material.dart'; void main() => runApp(const MyApp()); 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'), ), ), ); } }
新增標題部分
#在這一部分,建立一個 TitleSection 元件,使其符合以下佈局。
標題部分的草圖與 UI 原型
新增 TitleSection 元件
#
在 MyApp 類之後新增以下程式碼。
class TitleSection extends StatelessWidget {
const TitleSection({super.key, required this.name, required this.location});
final String name;
final String location;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Text(location, style: TextStyle(color: Colors.grey[500])),
],
),
),
/*3*/
Icon(Icons.star, color: Colors.red[500]),
const Text('41'),
],
),
);
}
}
- 為了使用行中所有剩餘的空白空間,請使用
Expanded元件來拉伸Column元件。若要將列放置在行的起始位置,請將crossAxisAlignment屬性設定為CrossAxisAlignment.start。 - 若要在文字行之間新增間距,請將這些行放入
Padding元件中。 - 標題行以一個紅色的星形圖示和文字
41結尾。整個行位於一個Padding元件內,並在每一邊設定了 32 畫素的內邊距。
將應用主體改為滾動檢視
#在 body 屬性中,將 Center 元件替換為 SingleChildScrollView 元件。在 SingleChildScrollView 元件內,將 Text 元件替換為 Column 元件。
body: const Center(
child: Text('Hello World'),
body: const SingleChildScrollView(
child: Column(
children: [
這些程式碼更新以以下方式改變了應用。
SingleChildScrollView元件可以滾動。這允許顯示當前螢幕放不下的元素。Column元件按列出的順序顯示其children屬性中的任何元素。列表中的第一個元素顯示在頂部。children列表中的元素按陣列順序從上到下顯示在螢幕上。
更新應用以顯示標題部分
#將 TitleSection 元件新增為 children 列表中的第一個元素。這會將其置於螢幕頂部。將提供的名稱和位置資訊傳遞給 TitleSection 建構函式。
children: [
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
],
新增按鈕部分
#在這一部分,新增將為您的應用增加功能的按鈕。
按鈕部分包含三列,它們使用相同的佈局:一個圖示上方放置一行文字。
按鈕部分的草圖與 UI 原型
計劃將這些列分佈在同一行中,以便每個列佔用相同的空間。將所有文字和圖示設為主題色(primary color)。
新增 ButtonSection 元件
#
在 TitleSection 元件之後新增以下程式碼,以包含構建按鈕行的程式碼。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
// ···
}
}
建立一個用於生成按鈕的元件
#由於每一列的程式碼可以使用相同的語法,因此建立一個名為 ButtonWithText 的元件。該元件的建構函式接受顏色、圖示資料和按鈕標籤。利用這些值,該元件構建了一個包含 Icon 和樣式化的 Text 元件作為其子元素的 Column。為了分隔這些子元素,Text 元件被包裹在一個 Padding 元件中。
在 ButtonSection 類之後新增以下程式碼。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
// ···
}
class ButtonWithText extends StatelessWidget {
const ButtonWithText({
super.key,
required this.color,
required this.icon,
required this.label,
});
final Color color;
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
}
使用 Row 元件定位按鈕
#
將以下程式碼新增到 ButtonSection 元件中。
- 新增三個
ButtonWithText元件例項,每個按鈕對應一個。 - 傳遞該特定按鈕的顏色、
Icon和文字。 - 使用
MainAxisAlignment.spaceEvenly值沿主軸對齊各列。Row元件的主軸是水平的,Column元件的主軸是垂直的。因此,該值告訴 Flutter 在Row中每一列的前面、中間和後面均勻地分配空白空間。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
return SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ButtonWithText(color: color, icon: Icons.call, label: 'CALL'),
ButtonWithText(color: color, icon: Icons.near_me, label: 'ROUTE'),
ButtonWithText(color: color, icon: Icons.share, label: 'SHARE'),
],
),
);
}
}
class ButtonWithText extends StatelessWidget {
const ButtonWithText({
super.key,
required this.color,
required this.icon,
required this.label,
});
final Color color;
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
// ···
);
}
}
更新應用以顯示按鈕部分
#將按鈕部分新增到 children 列表中。
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
ButtonSection(),
],
新增文字部分
#在這一部分,嚮應用新增文字描述。
文字塊的草圖與 UI 原型
新增 TextSection 元件
#
將以下程式碼作為獨立元件新增到 ButtonSection 元件之後。
class TextSection extends StatelessWidget {
const TextSection({super.key, required this.description});
final String description;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Text(description, softWrap: true),
);
}
}
透過將 softWrap 設定為 true,文字行會在單詞邊界處換行之前填滿列寬。
更新應用以顯示文字部分
#在 ButtonSection 之後新增一個新的 TextSection 元件作為子項。新增 TextSection 元件時,將其 description 屬性設定為地點描述文字。
location: 'Kandersteg, Switzerland',
),
ButtonSection(),
TextSection(
description:
'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.',
),
],
新增影像部分
#在這一部分,新增影像檔案以完成佈局。
配置應用以使用提供的影像
#若要配置應用以引用影像,請修改其 pubspec.yaml 檔案。
在專案根目錄建立一個
images目錄。-
下載
lake.jpg影像並將其新增到新的images目錄中。 -
若要包含影像,請在應用根目錄的
pubspec.yaml檔案中新增assets標籤。當您新增assets時,它將充當指向程式碼可用影像的指標集。pubspec.yamlyamlflutter: uses-material-design: true assets: - images/lake.jpg
建立 ImageSection 元件
#
在其他宣告之後定義以下 ImageSection 元件。
class ImageSection extends StatelessWidget {
const ImageSection({super.key, required this.image});
final String image;
@override
Widget build(BuildContext context) {
return Image.asset(image, width: 600, height: 240, fit: BoxFit.cover);
}
}
BoxFit.cover 值告訴 Flutter 在兩個約束條件下顯示影像。首先,儘可能顯示影像。其次,覆蓋佈局分配的所有空間,即所謂的渲染框(render box)。
更新應用以顯示影像部分
#將 ImageSection 元件新增為 children 列表中的第一個子項。將 image 屬性設定為您在配置應用以使用提供的影像中新增的影像路徑。
children: [
ImageSection(
image: 'images/lake.jpg',
),
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
恭喜
#就是這樣!當您熱過載應用時,您的應用應該看起來像這樣。
最終成品應用
資源
#您可以從以下位置獲取本教程中使用的資源:
Dart 程式碼: main.dart
影像: ch-photo
Pubspec: pubspec.yaml
後續步驟
#若要為該佈局新增互動性,請閱讀互動性教程。