跳至主要内容

Flutter 中的布局

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

您可以通过组合 Widget 来构建更复杂的 Widget 以创建布局。例如,下面的第一个屏幕截图显示了 3 个图标,每个图标下方都有一个标签。

Sample layout Sample layout with visual debugging

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

以下是此 UI 的 Widget 树图

Node tree

其中大部分应该如您所料,但您可能想知道容器(以粉红色显示)。Container是一个 Widget 类,允许您自定义其子 Widget。当您想要添加填充、边距、边框或背景颜色时,可以使用Container,仅举其一些功能。

在此示例中,每个Text Widget 都放置在一个Container中以添加边距。Row的整体也放置在一个Container中以在该行周围添加填充。

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

布局部件

#

如何在 Flutter 中布局单个 Widget?本节将向您展示如何创建和显示简单的 Widget。它还显示了简单的 Hello World 应用的完整代码。

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

1. 选择布局部件

#

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

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

2. 创建可见部件

#

例如,创建一个Text Widget。

dart
Text('Hello World'),

创建一个Image Widget。

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

创建一个Icon Widget。

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

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

#

所有布局 Widget 都有以下两种情况之一

  • 如果它们接受单个子 Widget,则为child属性,例如CenterContainer
  • 如果它们接受 Widget 列表,则为children属性,例如RowColumnListViewStack

Text Widget 添加到Center Widget 中。

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

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

#

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

Material 应用

#

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

dart
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 Widget。

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

  • 要设置默认颜色,请将配置好的CupertinoThemeData传递到应用的theme属性。

  • 要向应用顶部添加 iOS 风格的导航栏,请将CupertinoNavigationBar Widget 添加到脚手架的navigationBar属性。您可以使用CupertinoColors提供的颜色来配置 Widget 以匹配 iOS 设计。

  • 要布局应用的主体,请使用所需的 Widget 作为其值的脚手架的child属性,例如CenterColumn

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

dart
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 应用,您可以将Container Widget 添加到应用的build()方法中。

dart
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

垂直和水平布局多个部件

#

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

要在 Flutter 中创建行或列,您可以将子 Widget 列表添加到RowColumn Widget 中。依次,每个子 Widget 本身可以是行或列,依此类推。以下示例显示了如何在行或列内部嵌套行或列。

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

Screenshot with callouts showing the row containing two children

左侧列的 Widget 树嵌套了行和列。

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会在每个图像之间、之前和之后平均分配剩余的水平空间。

dart
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 会将剩余的垂直空间平均分配到每个图像之间、上方和下方。

dart
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 组件包装每个图像。

dart
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 属性,这是一个确定小部件弹性因子的整数。默认弹性因子为 1。以下代码将中间图像的弹性因子设置为 2。

dart
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。以下示例使用此属性将星形图标紧密排列在一起。

dart
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 个星形图标和文本的较小行的行。

dart
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 变量定义了图标行。

dart
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 的标题和文本。

dart
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() 嵌入网络上的图像,但在此示例中,图像保存到项目中的 images 目录中,添加到 pubspec 文件 中,并使用 Images.asset() 访问。有关更多信息,请参阅 添加资源和图像

dart
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 组件库。

标准部件

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

Material 部件

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

容器

#

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

摘要 (Container)

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

示例 (Container)

#

此布局由一个包含两行的列组成,每行包含 2 个图像。一个 Container 用于将列的背景颜色更改为浅灰色。

dart
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 还用于为每个图像添加圆角边框和边距。

dart
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.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

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)

#
  • 用于组织框列表的专用 Column
  • 可以水平或垂直布局
  • 检测其内容何时不适合并提供滚动功能
  • 不如 Column 可配置,但更易于使用并支持滚动

示例 (ListView)

#
ListView containing movie theaters and restaurants

使用 ListView 使用 ListTile 显示企业列表。Divider 将剧院与餐厅分隔开来。

应用源代码: grid_and_list

ListView containing shades of blue

使用 ListView 显示特定颜色系列的 Colors 来自 Material 2 设计调色板

Dart 代码: colors_demo.dart

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)

#
Circular avatar image with a label

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

应用源代码: card_and_stack

An image with a icon overlaid on top

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

Dart 代码: bottom_navigation_demo.dart

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

卡片

#

来自 Material 库Card 包含相关的信息片段,并且可以由几乎任何小部件组成,但通常与 ListTile 一起使用。Card 只有一个子组件,但其子组件可以是列、行、列表、网格或其他支持多个子组件的组件。默认情况下,Card 将其大小缩小到 0 x 0 像素。您可以使用 SizedBox 限制卡片的大小。

在 Flutter 中,Card 具有略微圆角和阴影,使其具有 3D 效果。更改 Cardelevation 属性可以控制阴影效果。例如,将 elevation 设置为 24 会在视觉上将 Card 从表面抬高,并导致阴影变得更加分散。有关支持的 elevation 值列表,请参阅 Elevation 中的 Material 指南。指定不受支持的值将完全禁用阴影。

摘要 (Card)

#
  • 实现了一个 Material 卡片
  • 用于呈现相关的信息片段
  • 接受一个子元素,但该子元素可以是 RowColumn 或其他包含子元素列表的 widget
  • 显示为圆角和阴影
  • Card 的内容不能滚动
  • 来自 Material 库

示例(卡片)

#
Card containing 3 ListTiles

一个包含 3 个 ListTiles 的 Card,并使用 SizedBox 包裹以调整大小。一个 Divider 分隔了第一个和第二个 ListTiles

应用源代码: card_and_stack

Tappable card containing an image and multiple forms of text

一个包含图像和文本的 Card

Dart 代码: cards_demo.dart

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,一个来自 Material 库 的专门的行 widget,可以轻松创建包含最多 3 行文本以及可选的前导和尾随图标的行。ListTile 最常用于 CardListView 中,但也可以用在其他地方。

总结(ListTile)

#
  • 一个包含最多 3 行文本和可选图标的专用行
  • Row 配置选项少,但更易于使用
  • 来自 Material 库

示例(ListTile)

#
Card containing 3 ListTiles

一个包含 3 个 ListTileCard

应用源代码: card_and_stack

4 ListTiles, each containing a leading avatar

使用带有前导 widget 的 ListTile

Dart 代码: list_demo.dart


约束

#

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

视频

#

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



Flutter in Focus 播放列表


每周 Widget 系列 的每一集都重点介绍一个 widget。其中一些包括布局 widget。


Flutter 每周 Widget 播放列表

其他资源

#

编写布局代码时,以下资源可能会有所帮助。

布局教程
了解如何构建布局。
部件目录
描述了 Flutter 中可用的许多 widget。
Flutter 中的 HTML/CSS 等效项
对于熟悉 Web 编程的人员,此页面将 HTML/CSS 功能映射到 Flutter 功能。
API 参考文档
所有 Flutter 库的参考文档。
添加资源和图像
说明如何将图像和其他资源添加到应用程序的包中。
使用 Flutter 从零到一
一个人编写他的第一个 Flutter 应用程序的体验。