跳到主內容

構建 Flutter 佈局

學習如何在 Flutter 中構建佈局。

本教程介紹如何在 Flutter 中設計和構建佈局。

如果您使用提供的示例程式碼,您可以構建出以下應用。

The finished app.

最終成品應用。

攝影:Dino Reichmuth (來自 Unsplash)。文字:瑞士旅遊局

若要更全面地瞭解佈局機制,請先從 Flutter 的佈局方案開始。

繪製佈局草圖

#

在這一部分,請思考您想要為應用使用者提供什麼樣的使用者體驗。

思考如何定位使用者介面的元件。佈局即這些定位的總和。考慮提前規劃佈局以加快編碼速度。使用視覺線索來確定螢幕元素的位置會有很大幫助。

無論您喜歡哪種方式(例如介面設計工具或紙筆),請在編寫程式碼之前先確定要在螢幕上放置元素的位置。這就是程式設計版的諺語:“三思而後行”。

  1. 問自己以下問題,將佈局拆解為基礎元素。

    • 你能識別出哪些是行,哪些是列嗎?
    • 佈局是否包含網格?
    • 是否有重疊的元素?
    • UI 是否需要標籤頁(tabs)?
    • 你需要對齊、新增內邊距(padding)或邊框的地方在哪裡?
  2. 識別出較大的元素。在這個示例中,您將影像、標題、按鈕和描述排列成一個列(Column)。

    Major elements in the layout: image, row, row, and text block

    佈局中的主要元素:影像、行、行和文字塊

  3. 為每一行繪製圖表。

    1. 第 1 行(標題部分)有三個子元素:一個文字列、一個星形圖示和一個數字。它的第一個子元素(列)包含兩行文字。第一列可能需要更多空間。

      Title section with text blocks and an icon

      帶有文字塊和圖示的標題部分

    2. 第 2 行(按鈕部分)有三個子元素:每個子元素包含一個列,該列中又包含一個圖示和一段文字。

      The Button section with three labeled buttons

      帶有三個標註按鈕的按鈕部分

規劃好佈局後,思考如何編寫程式碼。

你會把所有程式碼寫在一個類裡嗎?還是會為佈局的每個部分建立一個類?

遵循 Flutter 的最佳實踐,為佈局的每個部分建立一個類(或元件)。當 Flutter 需要重新渲染 UI 的一部分時,它會更新變化的最微小部分。這就是為什麼 Flutter “一切皆元件”。如果 Text 元件中只有文字改變了,Flutter 就只會重繪那部分文字。Flutter 會在響應使用者輸入時儘可能少地更改 UI。

在本教程中,請將您識別出的每個元素編寫為獨立的元件。

建立應用基礎程式碼

#

在這一部分,編寫 Flutter 應用的基礎程式碼以啟動應用。

  1. 設定您的 Flutter 環境.

  2. 建立一個新的 Flutter 應用.

  3. 用以下程式碼替換 lib/main.dart 的內容。此應用使用引數來設定應用標題,並顯示在應用的 appBar 中。這種方式簡化了程式碼。

    dart
    import '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 元件,使其符合以下佈局。

The Title section as sketch and prototype UI

標題部分的草圖與 UI 原型

新增 TitleSection 元件

#

MyApp 類之後新增以下程式碼。

dart
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'),
        ],
      ),
    );
  }
}
  1. 為了使用行中所有剩餘的空白空間,請使用 Expanded 元件來拉伸 Column 元件。若要將列放置在行的起始位置,請將 crossAxisAlignment 屬性設定為 CrossAxisAlignment.start
  2. 若要在文字行之間新增間距,請將這些行放入 Padding 元件中。
  3. 標題行以一個紅色的星形圖示和文字 41 結尾。整個行位於一個 Padding 元件內,並在每一邊設定了 32 畫素的內邊距。

將應用主體改為滾動檢視

#

body 屬性中,將 Center 元件替換為 SingleChildScrollView 元件。在 SingleChildScrollView 元件內,將 Text 元件替換為 Column 元件。

dart
body: const Center(
  child: Text('Hello World'),
body: const SingleChildScrollView(
  child: Column(
    children: [

這些程式碼更新以以下方式改變了應用。

  • SingleChildScrollView 元件可以滾動。這允許顯示當前螢幕放不下的元素。
  • Column 元件按列出的順序顯示其 children 屬性中的任何元素。列表中的第一個元素顯示在頂部。children 列表中的元素按陣列順序從上到下顯示在螢幕上。

更新應用以顯示標題部分

#

TitleSection 元件新增為 children 列表中的第一個元素。這會將其置於螢幕頂部。將提供的名稱和位置資訊傳遞給 TitleSection 建構函式。

dart
children: [
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',
  ),
],

新增按鈕部分

#

在這一部分,新增將為您的應用增加功能的按鈕。

按鈕部分包含三列,它們使用相同的佈局:一個圖示上方放置一行文字。

The Button section as sketch and prototype UI

按鈕部分的草圖與 UI 原型

計劃將這些列分佈在同一行中,以便每個列佔用相同的空間。將所有文字和圖示設為主題色(primary color)。

新增 ButtonSection 元件

#

TitleSection 元件之後新增以下程式碼,以包含構建按鈕行的程式碼。

dart
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 類之後新增以下程式碼。

dart
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 元件中。

  1. 新增三個 ButtonWithText 元件例項,每個按鈕對應一個。
  2. 傳遞該特定按鈕的顏色、Icon 和文字。
  3. 使用 MainAxisAlignment.spaceEvenly 值沿主軸對齊各列。Row 元件的主軸是水平的,Column 元件的主軸是垂直的。因此,該值告訴 Flutter 在 Row 中每一列的前面、中間和後面均勻地分配空白空間。
dart
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 列表中。

dart
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',
  ),
  ButtonSection(),
],

新增文字部分

#

在這一部分,嚮應用新增文字描述。

The text block as sketch and prototype UI

文字塊的草圖與 UI 原型

新增 TextSection 元件

#

將以下程式碼作為獨立元件新增到 ButtonSection 元件之後。

dart
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 屬性設定為地點描述文字。

dart
    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 檔案。

  1. 在專案根目錄建立一個 images 目錄。

  2. 下載 lake.jpg 影像並將其新增到新的 images 目錄中。

  3. 若要包含影像,請在應用根目錄的 pubspec.yaml 檔案中新增 assets 標籤。當您新增 assets 時,它將充當指向程式碼可用影像的指標集。

    pubspec.yaml
    yaml
    flutter:
      uses-material-design: true
      assets:
        - images/lake.jpg
    

建立 ImageSection 元件

#

在其他宣告之後定義以下 ImageSection 元件。

dart
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 屬性設定為您在配置應用以使用提供的影像中新增的影像路徑。

dart
children: [
  ImageSection(
    image: 'images/lake.jpg',
  ),
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',

恭喜

#

就是這樣!當您熱過載應用時,您的應用應該看起來像這樣。

The finished app

最終成品應用

資源

#

您可以從以下位置獲取本教程中使用的資源:

Dart 程式碼: main.dart
影像: ch-photo
Pubspec: pubspec.yaml

後續步驟

#

若要為該佈局新增互動性,請閱讀互動性教程