Skip to main content

布局

鉴于 Flutter 是一个 UI 工具包,您将花费大量时间使用 Flutter 小部件创建布局。在本节中,您将学习如何使用一些最常用的布局小部件构建布局。您将使用 Flutter DevTools(也称为 Dart DevTools)来了解 Flutter 如何创建您的布局。最后,您将遇到并调试 Flutter 最常见的布局错误之一,令人恐惧的“无界约束”错误。

理解 Flutter 中的布局

#

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

您可以通过组合小部件来构建更复杂的小部件从而创建布局。例如,下图显示了 3 个图标,每个图标下面都有一个标签,以及相应的小部件树:

一个显示小部件组合的图表,包含一系列线条和节点。

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

约束

#

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

一般来说,布局是指小部件的大小及其在屏幕上的位置。任何给定小部件的大小和位置都受其父级约束;它不能具有任何它想要的大小,并且它不决定自己在屏幕上的位置。相反,大小和位置是由小部件与其父级之间的对话决定的。

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

  1. 小部件从其父级接收其约束。
  2. 约束只是一组 4 个双精度数:最小和最大宽度,以及最小和最大高度。
  3. 小部件确定在这些约束内应为多大,并将它的宽度和高度传回父级。
  4. 父级查看它想要的大小以及它应该如何对齐,并相应地设置小部件的位置。对齐可以显式设置,使用各种小部件,如 Center,以及 RowColumn 上的对齐属性。

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

盒子类型

#

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

一般来说,有三种盒子:

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

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

其他小部件,例如 RowColumn(弹性盒子)根据给定的约束而变化。在理解约束文章中阅读更多关于弹性盒子和约束的信息。

布置单个小部件

#

要在 Flutter 中布置单个小部件,请使用可以更改其在屏幕上位置的小部件(例如 Center 小部件)包装可见小部件(例如 TextImage)。

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

下图显示了一个未在左侧对齐的小部件,以及一个已在右侧居中的小部件。

居中小部件的屏幕截图和未居中小部件的屏幕截图。

所有布局小部件都具有以下任一项:

  • 如果它们接受单个子元素,则具有 child 属性——例如,CenterContainerPadding
  • 如果它们接受小部件列表,则具有 children 属性——例如,RowColumnListViewStack

Container

#

Container 是一个便利的小部件,它由几个负责布局、绘制、定位和大小调整的小部件组成。关于布局,它可以用来向小部件添加填充和边距。还有一个 Padding 小部件可以在这里达到同样的效果。下面的示例使用 Container

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

下图显示了一个左侧没有填充的小部件,以及一个右侧有填充的小部件。

带有填充的小部件的屏幕截图和没有填充的小部件的屏幕截图。

要在 Flutter 中创建更复杂的布局,您可以组合多个小部件。例如,您可以组合 ContainerCenter

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

垂直或水平布局多个小部件

#

最常见的布局模式之一是垂直或水平排列小部件。您可以使用 Row 小部件水平排列小部件,使用 Column 小部件垂直排列小部件。本页上的第一个图同时使用了两者。

这是使用 Row 小部件的最基本示例。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}

带有三个子元素的行小部件的屏幕截图
此图显示了一个带有三个子元素的行小部件。

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

三个小部件的屏幕截图,每个小部件下面都有一个标签。
此图显示了一个带有三个子元素的行小部件,每个子元素都是一个列。

对齐行和列中的小部件

#

在下面的示例中,每个小部件的宽度均为 200 像素,视口的宽度为 700 像素。因此,小部件一个接一个地左对齐,右侧的所有额外空间。

一个显示三个小部件在一行中排列的图表。每个子小部件的宽度标记为 200px,右侧的空白空间标记为 100px 宽。

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

一个显示行和列中主轴和交叉轴方向的图表

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

dart
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}

三个小部件的屏幕截图,彼此之间均匀分布。
此图显示了一个带有三个子元素的行小部件,这些子元素使用 MainAxisAlignment.spaceEvenly 常量对齐。

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

使用列小部件垂直排列的三个小部件的屏幕截图。

MainAxisAlignmentCrossAxisAlignment 枚举提供了各种常量用于控制对齐方式。

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

行和列中小部件的大小调整

#

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

一个显示小部件行比其视口宽的屏幕截图。

可以通过使用 Expanded 小部件来调整小部件的大小以适应行或列。要修复前面图像行过宽而无法适应其渲染框的示例,请使用 Expanded 小部件包装每个图像。

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

三个小部件的屏幕截图,它们占据主轴上正好可用的空间量。所有三个小部件的宽度相等。
此图显示了一个带有三个子元素的行小部件,这些子元素都用 Expanded 小部件包装。

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

DevTools 和布局调试

#

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

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

在构建 Flutter 应用程序时,您可能会遇到的最常见错误是由于不正确地使用布局小部件造成的,这被称为“无界约束”错误。

如果您在开始构建 Flutter 应用时只应准备好应对一种类型错误,那就是这个。


解码 Flutter:无界高度和宽度

滚动小部件

#

Flutter 具有许多内置的小部件,这些小部件会自动滚动,并且还提供各种您可以自定义的小部件来创建特定的滚动行为。在本页上,您将看到如何使用最常见的小部件来使任何页面都可滚动,以及用于创建可滚动列表的小部件。

ListView

#

ListView 是一种类似列的小部件,当其内容长于其渲染框时,它会自动提供滚动。使用 ListView 的最基本方法与使用 ColumnRow 非常相似。与列或行不同,ListView 要求其子元素占据交叉轴上的所有可用空间,如下面的示例所示。

dart
Widget build(BuildContext context) {
  return ListView(
    children: const [
      BorderedImage(),
      BorderedImage(),
      BorderedImage(),
    ],
  );
}

垂直排列的三个小部件的屏幕截图。它们已扩展以占据交叉轴上的所有可用空间。
此图显示了一个带有三个子元素的 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),
          ],
        ),
      );
    },
  );
}

垂直排列的几个小部件的屏幕截图。它们已扩展以占据交叉轴上的所有可用空间。
此图显示了 ListView.builder 构造函数,用于显示未知数量的子元素。

自适应布局

#

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

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

构建器模式

#

在 Flutter 中,您会发现几个小部件在其名称或构造函数中使用“builder”一词。以下列表并非详尽无遗:

这些不同的“构建器”对于解决不同的问题很有用。例如,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();
      }
    },
  );
}

两个屏幕截图,其中一个显示窄布局,另一个显示宽布局。
此图显示了一个窄布局,它垂直布局其子元素,以及一个较宽的布局,它以网格形式布局其子元素。

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

此图显示了一个 `ListView`,其子元素具有交替的背景颜色。背景颜色是根据 `ListView` 中子元素的索引以编程方式确定的。
此图显示了一个 ListView,其子元素具有交替的背景颜色。背景颜色是根据 ListView 中子元素的索引以编程方式确定的。

其他资源

#

API 参考

#

以下资源解释各个 API。

反馈

#

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