Flutter 中的布局

Flutter 布局机制的核心是小部件。在 Flutter 中,几乎所有内容都是小部件,甚至布局模型也是小部件。您在 Flutter 应用中看到的图像、图标和文本都是小部件。但您看不到的内容也是小部件,例如排列、约束和对齐可见小部件的行、列和网格。

通过组合小部件来构建更复杂的小部件,从而创建布局。例如,下面的第一个屏幕截图显示了 3 个图标,每个图标下面都有一个标签

Sample layout Sample layout with visual debugging

第二个屏幕截图显示了可视布局,显示了 3 列一行,其中每列包含一个图标和一个标签。

以下是此 UI 的小组件树的图表

Node tree

其中大部分内容应符合您的预期,但您可能想知道容器(以粉红色显示)。Container 是一个允许您自定义其子小组件的小组件类。当您想添加内边距、外边距、边框或背景颜色(仅举几项功能)时,请使用 Container

在此示例中,每个 Text 小组件都放置在 Container 中以添加外边距。整个 Row 也放置在 Container 中以在行周围添加内边距。

此示例中的其余 UI 由属性控制。使用 color 属性设置 Icon 的颜色。使用 Text.style 属性设置字体、颜色、粗细等。列和行具有允许您指定其子元素如何垂直或水平对齐以及子元素应占据多少空间的属性。

布局小组件

如何在 Flutter 中布局单个小组件?本部分将向您展示如何创建和显示简单小组件。它还展示了简单 Hello World 应用程序的完整代码。

在 Flutter 中,只需几个步骤即可在屏幕上放置文本、图标或图像。

1. 选择布局小组件

根据您希望如何对齐或约束可见小组件,从各种 布局小组件 中进行选择,因为这些特性通常会传递给所包含的小组件。

此示例使用 Center,它将内容水平和垂直居中。

2. 创建可见小部件

例如,创建一个 Text 小部件

Text('Hello World'),

创建一个 Image 小部件

return Image.asset(
  image,
  fit: BoxFit.cover,
);

创建一个 Icon 小部件

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

3. 将可见小部件添加到布局小部件

所有布局小部件都具有以下任一属性

  • 如果它们采用单个子元素,则具有 child 属性,例如 CenterContainer
  • 如果它们采用小部件列表,则具有 children 属性,例如 RowColumnListViewStack

Text 小部件添加到 Center 小部件

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

4. 将布局小部件添加到页面

Flutter 应用本身是一个小部件,大多数小部件都有一个 build() 方法。在应用的 build() 方法中实例化并返回一个小部件会显示该小部件。

Material 应用

对于 Material 应用,可以使用 Scaffold 小部件;它提供默认横幅、背景颜色,并具有用于添加抽屉、小吃栏和底部工作表的 API。然后,可以将 Center 小部件直接添加到主页的 body 属性。

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

Cupertino 应用

要创建 Cupertino 应用,请使用 CupertinoAppCupertinoPageScaffold 小组件。

Material 不同,它不提供默认横幅或背景颜色。你需要自己设置这些内容。

  • 要设置默认颜色,请将已配置的 CupertinoThemeData 传递到应用的 theme 属性。
  • 要在应用顶部添加 iOS 风格的导航栏,请将 CupertinoNavigationBar 小组件添加到脚手架的 navigationBar 属性。你可以使用 CupertinoColors 提供的颜色来配置小组件,以匹配 iOS 设计。

  • 要布局应用的主体,请将脚手架的 child 属性设置为所需小组件的值,例如 CenterColumn

要了解可以添加的其他 UI 组件,请查看 Cupertino 库

lib/cupertino.dart (MyApp)
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter layout demo',
      theme: CupertinoThemeData(
        brightness: Brightness.light,
        primaryColor: CupertinoColors.systemBlue,
      ),
      home: CupertinoPageScaffold(
        navigationBar: CupertinoNavigationBar(
          backgroundColor: CupertinoColors.systemGrey,
          middle: Text('Flutter layout demo'),
        ),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Hello World'),
            ],
          ),
        ),
      ),
    );
  }
}

非 Material 应用

对于非 Material 应用,你可以将 Center 小组件添加到应用的 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 应用不包含 AppBar、标题或背景色。如果你想在非 Material 应用中使用这些功能,你必须自己构建它们。此应用将背景色更改为白色,将文本更改为深灰色,以模仿 Material 应用。

就是这样!当你运行应用时,你应该会看到Hello World

应用源代码

Hello World

垂直和水平排列多个小部件

最常见的布局模式之一是垂直或水平排列小部件。你可以使用 Row 小部件水平排列小部件,使用 Column 小部件垂直排列小部件。

要在 Flutter 中创建行或列,你可以将子小部件列表添加到 RowColumn 小部件中。反过来,每个子项本身可以是行或列,依此类推。以下示例展示了如何在行或列内嵌套行或列。

此布局组织为 Row。该行包含两个子项:左侧的列和右侧的图像

Screenshot with callouts showing the row containing two children

左侧列的小部件树嵌套行和列。

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

你将在 嵌套行和列 中实现一些 Pavlova 的布局代码。

对齐小部件

您可以使用 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

应用源: 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'),
  ],
);

应用源: row_column

Column showing 3 images spaced evenly

调整小部件大小

当布局太大而无法适应设备时,受影响的边缘会显示黄色和黑色条纹图案。这是一个行太宽的示例

Overly-wide row

可以通过使用Expanded小组件,将小组件调整到适合行或列中。为了修复图像行对其渲染框来说太宽的上一示例,请使用Expanded小组件包装每个图像。

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

应用源代码: sizing

也许您希望一个小组件占据其同级小组件两倍的空间。为此,请使用Expanded小组件的flex属性,这是一个决定小组件的 flex 因子的整数。默认的 flex 因子是 1。以下代码将中间图像的 flex 因子设置为 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

应用源代码: sizing

包装小组件

默认情况下,行或列在其主轴上占据尽可能多的空间,但如果您想紧密地打包子项,请将其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

应用源代码: pavlova

嵌套行和列

布局框架允许您将行和列嵌套在行和列中,深度可根据需要而定。让我们看看以下布局中轮廓部分的代码

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

轮廓部分实现为两行。评级行包含五个星号和评论数。图标行包含三列图标和文本。

评级行的组件树

Ratings row widget tree

ratings变量创建一行,其中包含一行较小的 5 个星形图标和文本

final 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 列;正如您在其组件树中看到的那样,每列包含一个图标和两行文本

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 变量包含评级和图标行,以及描述 Pavlova 的标题和文本

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

左列放置在 SizedBox 中以限制其宽度。最后,UI 使用包含左列和图像的整个行在 Card 中构建。

Pavlova 图像 来自 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,
        ],
      ),
    ),
  ),
),

应用源代码: pavlova


常见布局小组件

Flutter 拥有丰富的布局小组件库。以下是其中一些最常用的。目的是让你尽快上手,而不是用完整列表让你不知所措。有关其他可用小组件的信息,请参阅 小组件目录,或在 API 参考文档 中使用搜索框。此外,API 文档中的小组件页面通常会提出关于可能更适合你需求的类似小组件的建议。

以下小部件分为两类:小部件库中的标准小部件和Material 库中的专门小部件。任何应用都可以使用小部件库,但只有 Material 应用才能使用 Material Components 库。

标准小部件

  • Container:为小部件添加填充、边距、边框、背景颜色或其他装饰。
  • GridView:将小部件布局为可滚动的网格。
  • ListView:将小部件布局为可滚动的列表。
  • Stack:将一个小部件叠加在另一个小部件之上。

Material 小部件

  • Card:将相关信息整理到一个带有圆角和阴影的框中。
  • ListTile:将最多 3 行文本以及可选的前导和尾随图标整理到一行中。

Container

许多布局大量使用 Container 来使用填充分隔小部件,或添加边框或边距。你可以通过将整个布局放入 Container 并更改其背景颜色或图像来更改设备的背景。

摘要 (Container)

  • 添加填充、边距、边框
  • 更改背景颜色或图像
  • 包含一个子小部件,但该子小部件可以是行、列,甚至是小部件树的根
Diagram showing: margin, border, padding, and content

示例 (Container)

此布局包含两行的列,每行包含 2 张图像。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),
      ],
    );

你可以在 教程 中找到更多 Container 示例。

应用源代码: container


GridView

使用 GridView 以二维列表的形式排列小组件。 GridView 提供两个预制列表,或者你可以构建自己的自定义网格。当 GridView 检测到其内容过长而无法容纳渲染框时,它会自动滚动。

摘要 (GridView)

  • 以网格形式排列小组件
  • 检测到列内容超出渲染框时自动提供滚动
  • 构建自己的自定义网格,或使用提供的网格之一
    • GridView.count 允许你指定列数
    • GridView.extent 允许你指定一个图块的最大像素宽度

示例 (GridView)

A 3-column grid of photos

使用 GridView.extent 创建一个网格,其中图块最大宽度为 150 像素。

应用源代码: grid_and_list

A 2 column grid with footers

使用 GridView.count 创建一个网格,在纵向模式下为 2 列宽,在横向模式下为 3 列宽。通过为每个 GridTile 设置 footer 属性来创建标题。

Dart 代码: 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,一个类似于列的小组件,当其内容过长而无法容纳其渲染框时,会自动提供滚动。

摘要 (ListView)

  • 用于组织框列表的专门 Column
  • 可以水平或垂直排列
  • 检测到其内容不合适时提供滚动
  • 可配置性低于 Column,但更易于使用且支持滚动

示例 (ListView)

ListView containing movie theaters and restaurants

使用 ListView 使用 ListTile 显示商家列表。一个 Divider 将影院与餐厅分开。

应用源代码: grid_and_list

ListView containing shades of blue

使用 ListView 显示 Colors,这些颜色来自 Material 2 Design 调色板 的特定颜色系列。

Dart 代码: 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 将小部件排列在基础小部件(通常是图像)之上。小部件可以完全或部分覆盖基础小部件。

摘要(Stack)

  • 用于与其他小部件重叠的小部件
  • 子项列表中的第一个小部件是基础小部件;后续子项叠加在该基础小部件之上
  • 一个 Stack 的内容无法滚动
  • 你可以选择裁剪超出渲染框的子项

示例(Stack)

Circular avatar image with a label

使用 Stack 将一个 Container(在半透明黑色背景上显示其 Text)叠加在一个 CircleAvatar 之上。Stack 使用 alignment 属性和 Alignment 偏移文本。

应用源代码: card_and_stack

An image with a icon overlaid on top

使用 Stack 将一个图标叠加在图像之上。

Dart 代码: 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 包含相关的信息片段,并且几乎可以由任何小部件组成,但通常与 ListTile 一起使用。 Card 有一个子项,但其子项可以是列、行、列表、网格或支持多个子项的其他小部件。默认情况下, Card 会将大小缩小到 0 x 0 像素。您可以使用 SizedBox 来限制卡片的大小。

在 Flutter 中, Card 具有略微圆润的边角和阴影,从而产生 3D 效果。更改 Cardelevation 属性可以控制阴影效果。例如,将海拔高度设置为 24,在视觉上会将 Card 从表面进一步提升,并导致阴影变得更加分散。有关受支持的海拔高度值列表,请参阅 Material 指南 中的 海拔高度。指定不受支持的值会完全禁用阴影。

摘要(卡片)

  • 实现 Material 卡片
  • 用于呈现相关的信息片段
  • 接受单个子项,但该子项可以是 RowColumn 或其他包含子项列表的小部件
  • 显示为圆角和阴影
  • Card 的内容无法滚动
  • 来自 Material 库

示例(卡片)

Card containing 3 ListTiles

Card 包含 3 个 ListTiles,并通过用 SizedBox 包装来调整大小。 Divider 分隔第一个和第二个 ListTiles

应用源代码: card_and_stack

Tappable card containing an image and multiple forms of text

Card 包含图像和文本。

Dart 代码: 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('[email protected]'),
            leading: Icon(
              Icons.contact_mail,
              color: Colors.blue[500],
            ),
          ),
        ],
      ),
    ),
  );
}

ListTile

使用 ListTile(来自 Material 库 的一种专门的行小部件),可以轻松创建包含最多 3 行文本以及可选的前导和尾随图标的行。 ListTile 最常用于 CardListView 中,但也可以在其他地方使用。

摘要(ListTile)

  • 包含最多 3 行文本和可选图标的专门行
  • 可配置性低于 Row,但更易于使用
  • 来自 Material 库

示例(ListTile)

Card containing 3 ListTiles

包含 3 个 ListTileCard

应用源代码: card_and_stack

4 ListTiles, each containing a leading avatar

使用带有前置小组件的 ListTile

Dart 代码: list_demo.dart


约束

要完全理解 Flutter 的布局系统,您需要了解 Flutter 如何对布局中的组件进行定位和调整大小。有关更多信息,请参阅 了解约束

视频

以下视频是 Flutter in Focus 系列的一部分,解释了 StatelessStateful 小组件。

Flutter in Focus 播放列表


每集 Widget of the Week 系列 都重点介绍一个小组件。其中几个包括布局小组件。

Flutter Widget of the Week 播放列表

其他资源

以下资源可能有助于编写布局代码。