跳至主要内容

布局

鉴于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个双精度数:最小和最大宽度,以及最小和最大高度。
  3. widget确定在这些约束范围内应为多大,并将它的宽度和高度传递回父级。
  4. 父级查看它想要的大小以及它应该如何对齐,并相应地设置widget的位置。可以使用各种widget(如Center以及RowColumn上的对齐属性)显式设置对齐方式。

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

盒子类型

#

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

通常,有三种盒子

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

有些widget,例如Container,根据其构造函数参数的类型而异。Container构造函数默认为尝试尽可能大,但如果您为它指定了宽度,例如,它会尝试遵守该宽度并成为该特定大小。

其他,例如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 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.

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应用程序时只能准备应对一种类型错误,那将是这一种。


解码Flutter:无界高度和宽度

滚动widget

#

Flutter有许多内置的widget可以自动滚动,并且还提供各种您可以自定义的widget来创建特定的滚动行为。在本页中,您将了解如何使用最常见的widget使任何页面可滚动,以及用于创建可滚动列表的widget。

ListView

#

ListView 是一种类似列的部件,当其内容长度超过其渲染框时,会自动提供滚动功能。使用 ListView 的最基本方法与使用 ColumnRow 非常相似。与 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 部件。

当您有未知数量或非常多(或无限)的列表项时,通常会使用 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 应用,因此您可能需要根据屏幕尺寸或输入设备等因素调整应用程序的行为。这被称为使应用程序 *自适应* 和 *响应式*。

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

构建器模式

#

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

这些不同的“构建器”用于解决不同的问题。例如,ListView.builder 构造函数主要用于延迟渲染列表中的项,而 Builder 部件则用于在深层部件代码中访问 BuildContext

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

例如,LayoutBuilder 部件用于根据视口的大小创建响应式布局。构建器回调主体会接收到其父部件传递的 BoxConstraints,以及部件的“BuildContext”。使用这些约束,您可以根据可用空间返回不同的部件。


LayoutBuilder(Flutter 部件每周精选)

在下面的示例中,LayoutBuilder 返回的部件会根据视口是否小于或等于 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),
          ],
        ),
      );
    },
  );
}

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

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

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。

反馈

#

由于本网站的此部分正在发展,我们 欢迎您的反馈