跳到主內容

構建一個 Flutter 佈局

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

本教程解釋瞭如何在 Flutter 中設計和構建佈局。

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

The finished app.

完成的應用。

照片由 Dino ReichmuthUnsplash 上拍攝。文字由 瑞士旅遊局 提供。

為了更好地瞭解佈局機制,請從 Flutter 的佈局方法 開始。

繪製佈局圖

#

在本節中,請考慮您的應用使用者希望獲得哪種使用者體驗。

考慮如何定位使用者介面的元件。佈局由這些定位的總結果組成。考慮規劃您的佈局以加快編碼速度。使用視覺提示來了解某個元素在螢幕上的位置非常有幫助。

使用您喜歡的任何方法,例如介面設計工具或鉛筆和紙。在編寫程式碼之前,確定您希望將元素放置在螢幕上的位置。這是程式設計版的諺語:“三思而後行”。

  1. 提出這些問題來將佈局分解為基本元素。

    • 您能識別出行和列嗎?
    • 佈局是否包含網格?
    • 是否有重疊元素?
    • UI 是否需要選項卡?
    • 您需要對齊、填充還是邊框嗎?
  2. 識別較大的元素。在本例中,您將影像、標題、按鈕和描述排列成一列。

    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 最佳實踐,為佈局的每個部分建立一個類,或 Widget。當 Flutter 需要重新渲染 UI 的一部分時,它會更新發生更改的最小部分。這就是為什麼 Flutter 將“一切都做成 Widget”。如果 Text Widget 中的文字發生更改,Flutter 只會重新繪製該文字。Flutter 會以最少的更改來響應使用者輸入。

在本教程中,將您識別的每個元素都編寫成自己的 Widget。

建立應用基礎程式碼

#

在本節中,列出基本的 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 Widget。

The Title section as sketch and prototype UI

標題部分草圖和原型 UI

新增 TitleSection Widget

#

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 Widget 擴充套件 Column Widget。要將列放置在行的開頭,請將 crossAxisAlignment 屬性設定為 CrossAxisAlignment.start
  2. 要在文字行之間新增間距,請將這些行放在 Padding Widget 中。
  3. 標題行以紅色星形圖示和文字 41 結尾。整個行都位於 Padding Widget 內部,並且每個邊緣都填充 32 畫素。

將應用主體更改為可滾動檢視

#

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

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

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

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

更新應用以顯示標題部分

#

TitleSection Widget 作為 children 列表中的第一個元素。這將將其放置在螢幕的頂部。將提供的名稱和位置傳遞給 TitleSection 建構函式。

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

新增按鈕部分

#

在本節中,新增將為您的應用新增功能的按鈕。

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

The Button section as sketch and prototype UI

按鈕部分草圖和原型 UI

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

新增 ButtonSection Widget

#

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

dart
class ButtonSection extends StatelessWidget {
  const ButtonSection({super.key});

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    // ···
  }

}

建立一個用於製作按鈕的 Widget

#

由於每個列的程式碼可以使用相同的語法,因此建立一個名為 ButtonWithText 的 Widget。該 Widget 的建構函式接受顏色、圖示資料和按鈕標籤。使用這些值,該 Widget 構建一個帶有 Icon 和樣式化 Text Widget 作為子項的 Column。為了分隔這些子項,將 Text Widget 包裝在一個 Padding Widget 中。

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 Widget 定位按鈕

#

將以下程式碼新增到 ButtonSection Widget 中。

  1. 為每個按鈕新增三個 ButtonWithText Widget 的例項。
  2. 傳遞該特定按鈕的顏色、Icon 和文字。
  3. 使用 MainAxisAlignment.spaceEvenly 值沿主軸對齊列。Row Widget 的主軸是水平的,而 Column Widget 的主軸是垂直的。因此,此值告訴 Flutter 以相等數量在每個列之前、之間和之後排列空閒空間。
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 Widget

#

將以下程式碼作為單獨的 Widget 新增到 ButtonSection Widget 之後。

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,文字行將填充列寬,然後在單詞邊界處換行。

更新應用以顯示文字部分

#

將新的 TextSection Widget 作為 ButtonSection 之後的子項新增。在新增 TextSection Widget 時,將 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. 要包含影像,請將 assets 標籤新增到應用根目錄中的 pubspec.yaml 檔案中。當您新增 assets 時,它充當指向程式碼中可用影像的指標集。

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

建立 ImageSection Widget

#

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

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 使用兩個約束來顯示影像。首先,以儘可能小的尺寸顯示影像。其次,覆蓋佈局分配的所有空間,稱為渲染框。

更新應用以顯示影像部分

#

ImageSection Widget 作為 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

下一步

#

要為這個佈局新增互動性,請參考 互動教程