鉴于 Flutter 是一个 UI 工具包,你将花费大量时间使用 Flutter widget 创建布局。在本节中,你将学习如何使用一些最常见的布局 widget 构建布局。你将使用 Flutter DevTools(也称为 Dart DevTools)来理解 Flutter 如何创建你的布局。最后,你将遇到并调试 Flutter 最常见的布局错误之一,即可怕的“无限制约束”错误。

理解 Flutter 中的布局

#

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

你通过组合 widget 来构建更复杂的 widget,从而创建布局。例如,下图显示了 3 个图标,每个图标下方都有一个标签,以及相应的 widget 树

A diagram that shows widget composition with a series of lines and nodes.

在此示例中,有一个包含 3 列的行,每列包含一个图标和一个标签。所有布局,无论多么复杂,都是通过组合这些布局 widget 来创建的。

约束

#

理解 Flutter 中的约束是理解 Flutter 中布局工作原理的重要组成部分。

从广义上讲,布局是指 widget 的大小及其在屏幕上的位置。任何给定 widget 的大小和位置都受其父级的约束;它不能拥有任意大小,也无法自行决定在屏幕上的位置。相反,大小和位置是由 widget 及其父级之间的一场“对话”决定的。

在最简单的例子中,布局“对话”如下所示

  1. 一个 widget 从其父级接收其约束。
  2. 一个约束只是一组 4 个 double 值:最小宽度和最大宽度,以及最小高度和最大高度。
  3. widget 在这些约束范围内确定其应有的大小,并将其宽度和高度传递回父级。
  4. 父级查看它想要的大小以及它应该如何对齐,并相应地设置 widget 的位置。对齐可以明确设置,使用各种 widget,如 Center,以及 RowColumn 上的对齐属性。

在 Flutter 中,这种布局“对话”通常用简化的短语来表达:“约束向下传递。大小向上返回。父级设置位置。”

盒类型

#

在 Flutter 中,widget 由其底层的 RenderBox 对象渲染。这些对象决定如何处理传递给它们的约束。

通常,有三种类型的盒

  • 那些试图尽可能大的盒。例如,CenterListView 使用的盒。
  • 那些试图与其子级大小相同的盒。例如,TransformOpacity 使用的盒。
  • 那些试图成为特定大小的盒。例如,ImageText 使用的盒。

有些 widget,例如 Container,根据其构造函数参数的不同而有所不同。Container 构造函数默认尝试尽可能大,但如果你给它一个宽度,例如,它会尝试遵守该宽度并成为该特定大小。

其他 widget,例如 RowColumn(弹性盒),则根据它们所获得的约束而有所不同。在理解约束文章中阅读更多关于弹性盒和约束的信息。

布局单个 widget

#

要在 Flutter 中布局单个 widget,请将可见 widget(例如 TextImage)包装在一个可以改变其在屏幕上位置的 widget 中,例如 Center widget。

dart
Widget build(BuildContext context) {
  return Center(
    child: BorderedImage(),
  );
}

下图显示了左侧未对齐的 widget,以及右侧已居中的 widget。

A screenshot of a centered widget and a screenshot of a widget that hasn't been centered.

所有布局 widget 都具有以下其中一项

  • 如果它们接受单个子级,则具有 child 属性——例如,CenterContainerPadding
  • 如果它们接受 widget 列表,则具有 children 属性——例如,RowColumnListViewStack

容器

#

Container 是一个便利 widget,由多个负责布局、绘制、定位和大小调整的 widget 组成。在布局方面,它可用于为 widget 添加内边距(padding)和外边距(margins)。这里也可以使用 Padding widget 来达到同样的效果。以下示例使用 Container

dart
Widget build(BuildContext context) {
  return Container(
    padding: EdgeInsets.all(16.0),
    child: BorderedImage(),
  );
}

下图显示了左侧无内边距的 widget,以及右侧有内边距的 widget。

A screenshot of a widget with padding and a screenshot of a widget without padding.

要在 Flutter 中创建更复杂的布局,你可以组合许多 widget。例如,你可以组合 ContainerCenter

dart
Widget build(BuildContext context) {
  return Center(
    Container(
      padding: EdgeInsets.all(16.0),
      child: BorderedImage(),
    ),
  );
}

垂直或水平布局多个 widget

#

最常见的布局模式之一是垂直或水平排列 widget。你可以使用 Row widget 水平排列 widget,使用 Column widget 垂直排列 widget。本页上的第一张图就同时使用了这两种。

这是使用 Row widget 的最基本示例。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}
A screenshot of a row widget with three children
此图显示了一个带有三个子级的行 widget。

RowColumn 的每个子级本身都可以是行和列,组合起来形成复杂的布局。例如,你可以使用列为上述示例中的每个图像添加标签。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      Column(
        children: [
          BorderedImage(),
          Text('Dash 1'),
        ],
      ),
      Column(
        children: [
          BorderedImage(),
          Text('Dash 2'),
        ],
      ),
      Column(
        children: [
          BorderedImage(),
          Text('Dash 3'),
        ],
      ),
    ],
  );
}
A screenshot of a row of three widgets, each of which has a label underneath it.
此图显示了一个带有三个子级的行 widget,每个子级都是一个列。

在行和列中对齐 widget

#

在以下示例中,每个 widget 宽 200 像素,视口宽 700 像素。因此,这些 widget 从左到右依次对齐,所有额外空间都在右侧。

A diagram that shows three widgets laid out in a row. Each child widget is labeled as 200px wide, and the blank space on the right is labeled as 100px wide.

你可以使用 mainAxisAlignmentcrossAxisAlignment 属性来控制行或列如何对齐其子级。对于行,主轴水平运行,交叉轴垂直运行。对于列,主轴垂直运行,交叉轴水平运行。

A diagram that shows the direction of the main axis and cross axis in both rows and columns

将主轴对齐方式设置为 spaceEvenly 会在每个图像之间、之前和之后均匀分配水平自由空间。

dart
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}
A screenshot of three widgets, spaced evenly from each other.
此图显示了一个带有三个子级的行 widget,这些子级使用 MainAxisAlignment.spaceEvenly 常量进行对齐。

列的工作方式与行相同。以下示例显示了一个包含 3 张图像的列,每张图像高 100 像素。渲染盒(在本例中为整个屏幕)的高度超过 300 像素,因此将主轴对齐方式设置为 spaceEvenly 会在每个图像之间、上方和下方均匀分配垂直自由空间。

A screenshot of a three widgets laid out vertically, using a column widget.

A MainAxisAlignmentCrossAxisAlignment 枚举提供了多种常量来控制对齐。

Flutter 包含其他可用于对齐的 widget,特别是 Align widget。

在行和列中调整 widget 大小

#

当布局过大无法适应设备时,受影响的边缘会出现黄色和黑色条纹图案。在此示例中,视口宽 400 像素,每个子级宽 150 像素。

A screenshot of a row of widgets that are wider than their viewport.

通过使用 Expanded widget,可以调整 widget 大小以适应行或列。为了解决上一个示例中图像行对于其渲染盒来说过宽的问题,请将每个图像包装在一个 Expanded widget 中。

dart
Widget build(BuildContext context) {
  return const Row(
    children: [
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
    ],
  );
}
A screenshot of three widgets, which take up exactly the amount of space available on the main axis. All three widgets are equal width.
此图显示了一个带有三个子级的行 widget,这些子级被 `Expanded` widget 包裹。

Expanded widget 还可以指定一个 widget 相对于其同级应占用多少空间。例如,你可能希望某个 widget 占用其同级两倍的空间。为此,请使用 Expanded widget 的 flex 属性,这是一个整数,用于确定 widget 的弹性因子。默认弹性因子为 1。以下代码将中间图像的弹性因子设置为 2

dart
Widget build(BuildContext context) {
  return const Row(
    children: [
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        flex: 2,
        child: BorderedImage(width: 150, height: 150),
      ),
      Expanded(
        child: BorderedImage(width: 150, height: 150),
      ),
    ],
  );
}
A screenshot of three widgets, which take up exactly the amount of space available on the main axis. The widget in the center is twice as wide as the widgets on the left and right.
此图显示了一个带有三个子级的行 widget,这些子级被 `Expanded` widget 包裹。中间的子级将其 `flex` 属性设置为 2。

DevTools 和调试布局

#

在某些情况下,盒的约束是无限制的,或者说是无限的。这意味着最大宽度或最大高度被设置为 double.infinity。当给定无限制约束时,试图尽可能大的盒将无法正常工作,并且在调试模式下会抛出异常。

渲染盒最终出现无限制约束的最常见情况是在弹性盒(RowColumn)内,以及在可滚动区域(例如 ListView 和其他 ScrollView 子类)内。例如,ListView 试图扩展以适应其交叉方向上的可用空间(也许它是一个垂直滚动的块,并试图与其父级一样宽)。如果你将一个垂直滚动的 ListView 嵌套在一个水平滚动的 ListView 内部,则内部列表将尝试尽可能宽,这将是无限宽的,因为外部列表在该方向上是可滚动的。

在构建 Flutter 应用程序时,你可能会遇到的最常见错误是由于不正确使用布局 widget 造成的,该错误被称为“无限制约束”错误。

如果你在首次开始构建 Flutter 应用程序时只准备面对一种错误类型,那就是这种错误。

在新标签页中在 YouTube 上观看:“解码 Flutter:无限制的高度和宽度”

可滚动 widget

#

Flutter 拥有许多可自动滚动的内置 widget,还提供了多种可自定义的 widget,用于创建特定的滚动行为。在此页面上,你将看到如何使用最常见的 widget 来使任何页面可滚动,以及用于创建可滚动列表的 widget。

ListView

#

ListView 是一个类似列的 widget,当其内容超出渲染盒时会自动提供滚动。使用 ListView 的最基本方式与使用 ColumnRow 非常相似。与列或行不同,ListView 要求其子级占用交叉轴上的所有可用空间,如下面的示例所示。

dart
Widget build(BuildContext context) {
  return ListView(
    children: const [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}
A screenshot of three widgets laid out vertically. They have expanded to take up all available space on the cross axis.
此图显示了一个带有三个子级的 ListView widget。

当列表项数量未知或非常大(或无限)时,通常使用 ListView。在这种情况下,最好使用 ListView.builder 构造函数。builder 构造函数只构建当前屏幕上可见的子级。

在以下示例中,ListView 正在显示待办事项列表。待办事项是从存储库中获取的,因此待办事项的数量是未知的。

dart
final List<ToDo> items = Repository.fetchTodos();

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, idx) {
      var item = items[idx];
      return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(item.description),
            Text(item.isComplete),
          ],
        ),
      );
    },
  );
}
A screenshot of several widgets laid out vertically. They have expanded to take up all available space on the cross axis.
此图显示了 ListView.builder 构造函数,用于显示未知数量的子级。

自适应布局

#

由于 Flutter 用于创建移动、平板电脑、桌面 Web 应用程序,因此你可能需要根据屏幕大小或输入设备等因素调整应用程序的行为方式。这被称为使应用程序具有自适应性响应性

在创建自适应布局时,最有用的 widget 之一是 LayoutBuilder widget。LayoutBuilder 是 Flutter 中使用“构建器”模式的众多 widget 之一。

构建器模式

#

在 Flutter 中,你会发现一些 widget 的名称或构造函数中使用了“builder”一词。以下列表并不详尽

这些不同的“构建器”对于解决不同的问题很有用。例如,ListView.builder 构造函数主要用于延迟渲染列表中的项目,而 Builder widget 则有助于在深层 widget 代码中获取对 BuildContext 的访问权限。

尽管它们的使用场景不同,但这些构建器的工作方式是统一的。构建器 widget 和构建器构造函数都具有名为“builder”的参数(或类似名称,例如 ListView.builder 中的 itemBuilder),并且 builder 参数始终接受回调。此回调是一个构建器函数。构建器函数是向父 widget 传递数据的回调,父 widget 使用这些参数构建并返回子 widget。构建器函数总是至少传递一个参数——构建上下文——并且通常至少传递另一个参数。

例如,LayoutBuilder widget 用于根据视口大小创建响应式布局。构建器回调体被传递从其父级接收的 BoxConstraints,以及 widget 的“BuildContext”。利用这些约束,你可以根据可用空间返回不同的 widget。

在新标签页中在 YouTube 上观看:“LayoutBuilder (Flutter 每周 Widget)”

在以下示例中,LayoutBuilder 返回的 widget 会根据视口是否小于等于 600 像素或大于 600 像素而改变。

dart
Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
      if (constraints.maxWidth <= 600) {
        return _MobileLayout();
      } else {
        return _DesktopLayout();
      }
    },
  );
}
Two screenshots, in which one shows a narrow layout and the other shows a wide layout.
此图显示了一个窄布局,其子级垂直布局;以及一个宽布局,其子级以网格形式布局。

同时,ListView.builder 构造函数上的 itemBuilder 回调会传递构建上下文和一个 int。此回调为列表中的每个项目调用一次,int 参数表示列表项的索引。当 Flutter 构建 UI 时,itemBuilder 回调第一次被调用时,传递给函数的 int 为 0,第二次为 1,依此类推。

这允许你根据索引提供特定的配置。回顾上面使用 ListView.builder 构造函数的示例

dart
final List<ToDo> items = Repository.fetchTodos();

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, idx) {
      var item = items[idx];
      return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(item.description),
            Text(item.isComplete),
          ],
        ),
      );
    },
  );
}

此示例代码使用传递给构建器的索引从项目列表中获取正确的待办事项,然后将该待办事项的数据显示在从构建器返回的 widget 中。

为了说明这一点,以下示例更改了每隔一个列表项的背景颜色。

dart
final List<ToDo> items = Repository.fetchTodos();

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, idx) {
      var item = items[idx];
      return Container(
        color: idx % 2 == 0 ? Colors.lightBlue : Colors.transparent,
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(item.description),
            Text(item.isComplete),
          ],
        ),
      );
    },
  );
}
This figure shows a `ListView`, in which its children have alternating background colors. The background colors were determined programmatically based on the index of the child within the `ListView`.
此图显示了一个 `ListView`,其中其子级具有交替的背景颜色。背景颜色是根据子级在 `ListView` 中的索引以编程方式确定的。

额外资源

#

API 参考

#

以下资源解释了各个 API。

反馈

#

由于本网站的此部分正在不断完善中,我们欢迎你的反馈