Flutter 中的佈局

Flutter 佈局的核心機制是 widgets。在 Flutter 中,幾乎所有東西都是 widget —— 甚至佈局模型都是 widgets。你在 Flutter 應用程式中看到的影象,圖示和文字都是 widgets。此外不能直接看到的也是 widgets,例如用來排列、限制和對齊可見 widgets 的行、列和網格。

你可以透過組合 widgets 來建構更復雜的 widgets 來建立佈局。比如,下面第一個截圖上有 3 個圖示,每個圖示下面都有一個標籤:

Sample layout Sample layout with visual debugging

第二個截圖顯示了可視佈局,可以看到有一排三列,其中每列包含一個圖示和一個標籤。

以下是這個 UI 的 widget 樹形圖:

Node tree

圖上大部分應該和你預想的一樣,但你可能會疑惑 containers(圖上粉色顯示的)是什麼。 Container 是一個 widget,允許你自訂其子 widget。舉幾個例子,如果要新增 padding、margin、邊框或背景顏色,你就可以用上 Container 了。

在這個例子中,每個 Text widget 都被放在一個 Container 以新增 padding。整個 Row 也被放在一個 Container 中,以便新增 padding。

這個例子其餘部分的 UI 由屬性控制。透過 Iconcolor 屬性來設定它的顏色,透過 Text.style 屬性來設定文字的字型、顏色、字重等等。列和行有一些屬性可以讓你指定子項垂直或水平的對齊方式以及子項應占用的空間大小。

佈局 widget

如何在 Flutter 中佈局單個 widget?本節將介紹如何建立和顯示單個 widget。本節還包括一個簡單的 Hello World app 的完整程式碼。

在 Flutter 中,只需幾步就可以在螢幕上顯示文字、圖示或影象。

1. 選擇一個佈局 widget

根據你想要對齊或限制可見 widget 的方式從各種 layout widgets 中進行選擇,因為這些特性通常會傳遞它所給包含的 widget。

本例使用將其內容水平和垂直居中的 Center

2. 建立一個可見 widget

舉個例子,建立一個 Text widget:

Text('Hello World'),

建立一個 Image widget:

Image.asset(
  'images/lake.jpg',
  fit: BoxFit.cover,
),

建立一個 Icon widget:

Icon(
  Icons.star,
  color: Colors.red[500],
),

3. 將可見 widget 新增到佈局 widget

所有佈局 widgets 都具有以下任一項:

  • 一個 child 屬性,如果它們只包含一個子項 —— 例如 CenterContainer

  • 一個 children 屬性,如果它們包含多個子項 —— 例如 RowColumnListViewStack

Text widget 新增進 Center widget:

const Center(
  child: Text('Hello World'),
),

4. 將佈局 widget 新增到頁面

一個 Flutter app 本身就是一個 widget,大多數 widgets 都有一個 build() 方法,在 app 的 build() 方法中例項化和返回一個 widget 會讓它顯示出來。

對於 Material app,你可以使用 Scaffold widget,它提供預設的 banner 背景顏色,還有用於新增抽屜、提示條和底部列表彈窗的 API。你可以將 Center widget 直接新增到主頁 body 的屬性中。

lib/main.dart (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'),
        ),
      ),
    );
  }
}

非 Material apps

對於非 Material app,你可以將 Center widget 新增到 app 的 build() 方法裡:

lib/main.dart (MyApp)
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,
          ),
        ),
      ),
    );
  }
}

預設情況下,非 Material app 不包含 AppBar、標題和背景顏色。如果你希望在非 Material app 中使用這些功能,則必須自己建構它們。以上 app 將背景顏色更改為白色,將文字更改為深灰色來模擬一個 Material app。

完成! 啟動這個 app,你應該能看到 Hello World

App 原始碼:

Hello World

橫向或縱向佈局多個 widgets

最常見的佈局模式之一是垂直或水平 widgets。你可以使用 Row widget 水平排列 widgets,使用 Column widget 垂直排列 widgets。

要在 Flutter 中建立行或列,可以將子 widgets 列表新增到 RowColumn widget 中。反過來,每個子項本身可以是一行或一列,依此類推。以下範例示範瞭如何在行或列中巢狀(Nesting)行或列。

這個佈局被組織為 Row。這一行包含兩個子項:左側的列和右側的影象:

Screenshot with callouts showing the row containing two children

左側列的 widget 樹巢狀(Nesting)著行和列。

Diagram showing a left column broken down to its sub-rows and sub-columns

你將在 巢狀(Nesting)行和列 中實現蛋糕介紹範例的一些佈局程式碼。

對齊 widgets

你可以使用 mainAxisAlignmentcrossAxisAlignment 屬性控制行或列如何對齊其子項。對於一行來說,主軸水平延伸,交叉軸垂直延伸。對於一列來說,主軸垂直延伸,交叉軸水平延伸。

Diagram showing the main axis and cross axis for a row Diagram showing the main axis and cross axis for a column

MainAxisAlignmentCrossAxisAlignment 這兩個列舉提供了很多用於控制對齊的常量。

在以下範例中,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 with 3 evenly spaced images

App 原始碼: 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'),
  ],
);

App 原始碼: row_column

Column showing 3 images spaced evenly

調整 widgets 大小

當某個佈局太大而超出螢幕時,受影響的邊緣會出現黃色和黑色條紋圖案。這裡有一個行太寬的 例子

Overly-wide row

透過使用 Expanded widget,可以調整 widgets 的大小以適合行或列。要修復上一個影象行對其渲染框來說太寬的範例,可以用 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'),
    ),
  ],
);

Row of 3 images that are too wide, but each is constrained to take only 1/3 of the space

App 原始碼: 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'),
    ),
  ],
);

Row of 3 images with the middle image twice as wide as the others

App 原始碼: sizing

組合 widgets

預設情況下,行或列沿其主軸會佔用盡可能多的空間,但如果要將子項緊密組合在一起,請將其 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),
  ],
)

Row of 5 stars, packed together in the middle of the row

App 原始碼: pavlova

巢狀(Nesting)行和列

佈局框架允許你根據需要在行和列內巢狀(Nesting)行和列。讓我們看看以下佈局的概述部分的程式碼:

Screenshot of the pavlova app, with the ratings and icon rows outlined in red

概述的部分實現為兩行,評級一行包含五顆星和評論的數量,圖示一行包含由圖示與文字組成的三列。

以下是評級行的 widget 樹形圖:

Ratings row widget tree

ratings 變數建立了一個行,其中包含較小的由 5 個星形圖示和文字組成的一行:

var 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 樹中看到:

Icon widget tree

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 變數包含評級和圖示行,以及蛋糕介紹的標題和文字:

final leftColumn = Container(
  padding: const EdgeInsets.fromLTRB(20, 30, 20, 20),
  child: Column(
    children: [
      titleText,
      subTitle,
      ratings,
      iconList,
    ],
  ),
);

左列放置在 Container 中以限制其寬度。最後,UI 由 Card 內的整行(包含左列和影象)構成。

蛋糕圖片 來自 Pixabay 網站。你可以使用 Image.network() 從網路上參考影象,但是在本例影象將儲存到專案中的一個影象目錄中,新增到 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,
        ],
      ),
    ),
  ),
),

App 原始碼: pavlova


通用佈局 widgets

Flutter 有一個豐富的佈局 widget 儲存庫,裡面有很多經常會用到的佈局 widget。目的是為了讓你更快的上手,而不是被一個完整的列表嚇跑。關於其他有用的 widget 的資訊,可以參考 Widget 目錄,或者使用 API 參考文件 中的搜尋框。而且,API 文件中的 widget 頁面中經常會給出一些關於相似的 widget 哪個會更適合你的建議。

下面的 widget 會分為兩類:widgets 庫 中的標準 widgets 和 Material 庫 中的 widgets。任何 app 都可以使用 widget 庫,但是 Material 庫中的元件只能在 Material app 中使用。

標準 widgets

  • Container:向 widget 增加 padding、margins、borders、background color 或者其他的“裝飾”。

  • GridView:將 widget 展示為一個可滾動的網格。

  • ListView:將 widget 展示為一個可滾動的列表。

  • Stack:將 widget 覆蓋在另一個的上面。

Material widgets

  • Card:將相關資訊整理到一個有圓角和陰影的盒子中。

  • ListTile:將最多三行的文字、可選的導語以及後面的圖示組織在一行中。

Container

許多佈局都可以隨意的用 Container,它可以將使用了 padding 或者增加了 borders/margins 的 widget 分開。你可以透過將整個佈局放到一個 Container 中,並且改變它的背景色或者圖片,來改變裝置的背景。

摘要 (Container)

  • 增加 padding、margins、borders

  • 改變背景色或者圖片

  • 只包含一個子 widget,但是這個子 widget 可以是行、列或者是 widget 樹的根 widget

Diagram showing: margin, border, padding, and content

範例 (Container)

這個佈局包含一個有兩行的列,每行有兩張圖片。 Container 用來將列的背景色變為淺灰色。

Widget _buildImageColumn() {
  return Container(
    decoration: const BoxDecoration(
      color: Colors.black26,
    ),
    child: Column(
      children: [
        _buildImageRow(1),
        _buildImageRow(3),
      ],
    ),
  );
}
Screenshot showing 2 rows, each containing 2 images

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),
      ],
    );

你可以在 佈局建構課程Flutter Gallery 中可以發現更多關於 Container 的例子。

App 原始碼: container


GridView

使用 GridView 將 widget 作為二維列表展示。 GridView 提供兩個預製的列表,或者你可以自訂網格。當 GridView 檢測到內容太長而無法適應渲染盒時,它就會自動支援滾動。

摘要 (GridView)

  • 在網格中使用 widget

  • 當列的內容超出渲染容器的時候,它會自動支援滾動。

  • 建立自訂的網格,或者使用下面提供的網格的其中一個:

    • GridView.count 允許你制定列的數量

    • GridView.extent 允許你制定單元格的最大寬度

範例 (GridView)

A 3-column grid of photos

使用 GridView.extent 建立一個最大寬度為 150 畫素的網格。

App 原始碼: grid_and_list

A 2 column grid with footers

使用 GridView.count 建立一個網格,它在豎屏模式下有兩行,在橫屏模式下有三行。可以透過為每個 GridTile 設定 footer 屬性來建立標題。

Dart 程式碼: Flutter Gallery 中的 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<Container> _buildGridTileList(int count) => List.generate(
    count, (i) => Container(child: Image.asset('images/pic$i.jpg')));

ListView

ListView,一個和列很相似的 widget,當內容長於自己的渲染盒時,就會自動支援滾動。

摘要 (ListView)

  • 一個用來組織盒子中列表的專用 Column

  • 可以水平或者垂直佈局

  • 當監測到空間不足時,會提供滾動

  • Column 的配置少,使用更容易,並且支援滾動

範例 (ListView)

ListView containing movie theaters and restaurants

使用 ListView 的業務列表,它使用了多個 ListTileDivider 將餐廳從劇院中分隔開。

App 原始碼: grid_and_list

ListView containing shades of blue

使用 ListView 展示特定顏色系列 Material Design 調色盤 中的 Colors

Dart 程式碼: Flutter Gallery 中的 colors_demo.dart

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

可以使用 Stack 在基礎 widget(通常是圖片)上排列 widget, widget 可以完全或者部分覆蓋基礎 widget。

摘要 (Stack)

  • 用於覆蓋另一個 widget

  • 子列表中的第一個 widget 是基礎 widget;後面的子項覆蓋在基礎 widget 的頂部

  • Stack 的內容是無法滾動的

  • 你可以剪下掉超出渲染框的子項

範例 (Stack)

Circular avatar image with a label

CircleAvatar 的上面使用 Stack 覆蓋 Container (在透明的黑色背景上展示它的 Text)。 Stack 使用 alignment 屬性和 Alignment 讓文字偏移。

App 原始碼: card_and_stack

An image with a icon overlaid on top

使用 Stack 將漸變疊加到圖片的頂部,漸變可以將工具欄的圖示和圖片區分開來。

Dart 程式碼: Flutter Gallery 中的 bottom_navigation_demo.dart

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 控制 card 的大小。

在 Flutter 中,Card 有輕微的圓角和陰影來使它具有 3D 效果。改變 Cardelevation 屬性可以控制陰影效果。例如,把 elevation 設定為 24,可以從視覺上更多的把 Card 抬離表面,使陰影變得更加分散。關於支援的 elevation 的值的列表,可以檢視 Material guidelines 中的 Elevation。使用不支援的值則會使陰影無效。

摘要 (Card)

  • 實現一個 Material card

  • 用於呈現相關有價值的資訊

  • 接收單個子項,但是子項可以是 RowColumn 或者其他可以包含列表子項的 widget

  • 顯示圓角和陰影

  • Card 的內容無法滾動

  • 來自 Material 庫

範例 (Card)

Card containing 3 ListTiles

包含 3 個 ListTile 的 Card,並且透過被 SizedBox 包住來調整大小。 Divider 分隔了第一個和第二個 ListTiles

App 原始碼: card_and_stack

Card containing an image, text and buttons

包含圖片和文字的 Card

Dart 程式碼: Flutter Gallery 中的 cards_demo.dart

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

ListTileMaterial 庫 中專用的行 widget,它可以很輕鬆的建立一個包含三行文字以及可選的行前和行尾圖示的行。 ListTileCard 或者 ListView 中最常用,但是也可以在別處使用。

摘要 (ListTile)

  • 一個可以包含最多 3 行文字和可選的圖示的專用的行

  • Row 更少的配置,更容易使用

  • 來自 Material 庫

範例 (ListTile)

Card containing 3 ListTiles

包含 3 個 ListTilesCard

App 原始碼: card_and_stack

4 ListTiles, each containing a leading avatar

leading wiget 使用 ListTile

Dart 程式碼: Flutter Gallery 中的 list_demo.dart


Constraints

To fully understand Flutter’s layout system, you need to learn how Flutter positions and sizes the components in a layout. For more information, see Understanding constraints.

影片

下面的影片是 Flutter in Focus 系列的一部分,解釋了 Stateless 和 Stateful 的 widget。

Flutter in Focus playlist


每週 Widget 系列 的每一集都會介紹一個 widget。其中也包括一些佈局的 widget。

Flutter Widget of the Week playlist

其他資源

當寫佈局程式碼時,下面的資源可能會幫助到你。