概述

#

Flutter 布局机制的核心是组件。在 Flutter 中,几乎所有东西都是组件——即使布局模型也是组件。你在 Flutter 应用中看到的图片、图标和文本都是组件。但你看不见的东西也是组件,例如用于排列、约束和对齐可见组件的行、列和网格。你通过组合组件来构建更复杂的组件以创建布局。

概念示例

#

在以下示例中,第一个截图显示了带有标签的三个图标,第二个截图包括行和列的视觉布局。在第二个截图,debugPaintSizeEnabled 被设置为 true,这样你就可以看到视觉布局。

Sample layout
Sample layout with visual debugging

这是上一个示例的组件树图

Node tree

大部分内容应该如你所预料,但你可能对容器(显示为粉色)感到疑惑。Container 是一个组件类,允许你自定义其子组件。当你想添加内边距、外边距、边框或背景颜色等功能时,可以使用 Container

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

用户界面的其余部分由属性控制。使用 Iconcolor 属性设置其颜色。使用 Text.style 属性设置字体、其颜色、粗细等。列和行具有允许你指定其子项垂直或水平对齐方式以及子项应占用多少空间的属性。

布局单个组件

#

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

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

1. 选择一个布局组件

#

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

例如,你可以使用 Center 布局组件将可见组件水平和垂直居中

dart
Center(
  // Content to be centered here.
)

2. 创建一个可见组件

#

为你的应用选择一个可见组件,以包含可见元素,例如文本图像图标

例如,你可以使用 Text 组件显示一些文本

dart
Text('Hello World')

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

#

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

  • 如果它们接受单个子组件,则具有 child 属性——例如 CenterContainer
  • 如果它们接受组件列表,则具有 children 属性——例如 RowColumnListViewStack

Text 组件添加到 Center 组件

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

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

#

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

对于普通应用,你可以将 Container 组件添加到应用的 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),
        ),
      ),
    );
  }
}

默认情况下,普通应用不包含 AppBar、标题或背景颜色。如果你想在普通应用中实现这些功能,你必须自己构建它们。这个应用将背景颜色更改为白色,文本更改为深灰色,以模仿 Material 应用。

对于 Material 应用,你可以使用 Scaffold 组件;它提供默认横幅、背景颜色,并具有用于添加抽屉、小吃栏和底部工作表(bottom sheets)的 API。然后你可以将 Center 组件直接添加到主页的 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 应用,请使用 CupertinoAppCupertinoPageScaffold 组件。

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

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

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

  • 要布局应用的 body,请将脚手架的 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: [Text('Hello World')],
          ),
        ),
      ),
    );
  }
}

5. 运行你的应用

#

添加完组件后,运行你的应用。当你运行应用时,应该会看到 Hello World

应用源代码

Screenshot of app displaying 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 会将自由水平空间均匀地分配到每张图像之间、之前和之后。

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'),
  ],
);
Column showing 3 images spaced evenly

应用来源:row_column

调整组件大小

#

当布局过大无法适应设备时,受影响的边缘会出现黄黑相间的条纹图案。这是一个行过宽的示例

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 属性,这是一个整数,用于确定组件的 flex 系数。默认的 flex 系数是 1。以下代码将中间图像的 flex 系数设置为 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 中以限制其宽度。最后,使用包含左列和图像的整个行在 Card 内部构建 UI。

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 Components 库。

容器
为组件添加内边距、外边距、边框、背景颜色或其他装饰。
GridView
将组件布局为可滚动的网格。
ListView
将组件布局为可滚动的列表。
层叠布局
将一个组件叠在另一个组件之上。
Scaffold
提供一个结构化的布局框架,带有用于常见 Material Design 应用元素的插槽。
AppBar
创建一个通常显示在屏幕顶部的水平栏。
Card
将相关信息组织成一个带有圆角和阴影的框。
ListTile
将最多 3 行文本以及可选的前导和尾随图标组织成一行。
CupertinoPageScaffold
为 iOS 风格的页面提供基本布局结构。
CupertinoNavigationBar
在屏幕顶部创建 iOS 风格的导航栏。
CupertinoSegmentedControl
创建一个用于选择的分段控件。
CupertinoTabBarCupertinoTabScaffold
创建 iOS 特有的底部选项卡栏。

容器

#

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

总结 (Container)

#
  • 添加内边距、外边距、边框
  • 更改背景颜色或图像
  • 包含一个子组件,但该子组件可以是 RowColumn,甚至是组件树的根
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)

#
  • 以网格形式布局组件
  • 检测到列内容超出渲染框时自动提供滚动
  • 构建你自己的自定义网格,或使用提供的网格之一
    • 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 个瓦片的网格。标题是通过设置每个 GridTilefooter 属性创建的。

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<Widget> _buildGridTileList(int count) =>
    List.generate(count, (i) => Image.asset('images/pic$i.jpg'));

ListView

#

ListView 是一个类似列的组件,当其内容对于渲染框来说太长时,会自动提供滚动。

总结 (ListView)

#
  • 一个用于组织盒子列表的专用Column
  • 可以水平或垂直布局
  • 检测到其内容不适合时提供滚动
  • Column 配置性差,但更容易使用并支持滚动

示例 (ListView)

#
ListView containing movie theaters and restaurants

使用 ListViewListTile 显示商家列表。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 属性和 Alignments 偏移文本。

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

Card

#

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

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

总结 (Card)

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

示例 (Card)

#
Card containing 3 ListTiles

一个包含 3 个 ListTile 的 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('costa@example.com'),
            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 组件。

在新标签页中在 YouTube 上观看:“如何创建无状态组件”

在新标签页中在 YouTube 上观看:“有状态组件的最佳使用方式和时机”

Flutter in Focus 播放列表


每周组件系列的每一集都专注于一个组件。其中一些包括布局组件。

在新标签页中在 YouTube 上观看:“介绍每周组件”

Flutter 每周组件播放列表

其他资源

#

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

布局教程
学习如何构建布局。
组件目录
描述了 Flutter 中可用的许多组件。
Flutter 中的 HTML/CSS 类比
对于熟悉 Web 编程的人来说,此页面将 HTML/CSS 功能映射到 Flutter 特性。
API 参考文档
所有 Flutter 库的参考文档。
添加资产和图像
解释如何将图像和其他资产添加到你的应用包中。
Flutter 从零到一
一个人编写第一个 Flutter 应用的经验。