布局
鉴于 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 树

在此示例中,有一个包含 3 列的行,每列包含一个图标和一个标签。所有布局,无论多么复杂,都是通过组合这些布局 widget 来创建的。
约束
#理解 Flutter 中的约束是理解 Flutter 中布局工作原理的重要组成部分。
从广义上讲,布局是指 widget 的大小及其在屏幕上的位置。任何给定 widget 的大小和位置都受其父级的约束;它不能拥有任意大小,也无法自行决定在屏幕上的位置。相反,大小和位置是由 widget 及其父级之间的一场“对话”决定的。
在最简单的例子中,布局“对话”如下所示
- 一个 widget 从其父级接收其约束。
- 一个约束只是一组 4 个 double 值:最小宽度和最大宽度,以及最小高度和最大高度。
- widget 在这些约束范围内确定其应有的大小,并将其宽度和高度传递回父级。
- 父级查看它想要的大小以及它应该如何对齐,并相应地设置 widget 的位置。对齐可以明确设置,使用各种 widget,如
Center
,以及Row
和Column
上的对齐属性。
在 Flutter 中,这种布局“对话”通常用简化的短语来表达:“约束向下传递。大小向上返回。父级设置位置。”
盒类型
#在 Flutter 中,widget 由其底层的 RenderBox
对象渲染。这些对象决定如何处理传递给它们的约束。
通常,有三种类型的盒
- 那些试图尽可能大的盒。例如,
Center
和ListView
使用的盒。 - 那些试图与其子级大小相同的盒。例如,
Transform
和Opacity
使用的盒。 - 那些试图成为特定大小的盒。例如,
Image
和Text
使用的盒。
有些 widget,例如 Container
,根据其构造函数参数的不同而有所不同。Container
构造函数默认尝试尽可能大,但如果你给它一个宽度,例如,它会尝试遵守该宽度并成为该特定大小。
其他 widget,例如 Row
和 Column
(弹性盒),则根据它们所获得的约束而有所不同。在理解约束文章中阅读更多关于弹性盒和约束的信息。
布局单个 widget
#要在 Flutter 中布局单个 widget,请将可见 widget(例如 Text
或 Image
)包装在一个可以改变其在屏幕上位置的 widget 中,例如 Center
widget。
Widget build(BuildContext context) {
return Center(
child: BorderedImage(),
);
}
下图显示了左侧未对齐的 widget,以及右侧已居中的 widget。

所有布局 widget 都具有以下其中一项
- 如果它们接受单个子级,则具有
child
属性——例如,Center
、Container
或Padding
。 - 如果它们接受 widget 列表,则具有
children
属性——例如,Row
、Column
、ListView
或Stack
。
容器
#Container
是一个便利 widget,由多个负责布局、绘制、定位和大小调整的 widget 组成。在布局方面,它可用于为 widget 添加内边距(padding)和外边距(margins)。这里也可以使用 Padding
widget 来达到同样的效果。以下示例使用 Container
。
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: BorderedImage(),
);
}
下图显示了左侧无内边距的 widget,以及右侧有内边距的 widget。

要在 Flutter 中创建更复杂的布局,你可以组合许多 widget。例如,你可以组合 Container
和 Center
Widget build(BuildContext context) {
return Center(
Container(
padding: EdgeInsets.all(16.0),
child: BorderedImage(),
),
);
}
垂直或水平布局多个 widget
#最常见的布局模式之一是垂直或水平排列 widget。你可以使用 Row
widget 水平排列 widget,使用 Column
widget 垂直排列 widget。本页上的第一张图就同时使用了这两种。
这是使用 Row
widget 的最基本示例。
Widget build(BuildContext context) {
return Row(
children: [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}

Row
或 Column
的每个子级本身都可以是行和列,组合起来形成复杂的布局。例如,你可以使用列为上述示例中的每个图像添加标签。
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'),
],
),
],
);
}

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

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

将主轴对齐方式设置为 spaceEvenly
会在每个图像之间、之前和之后均匀分配水平自由空间。
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}

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

A MainAxisAlignment
和 CrossAxisAlignment
枚举提供了多种常量来控制对齐。
Flutter 包含其他可用于对齐的 widget,特别是 Align
widget。
在行和列中调整 widget 大小
#当布局过大无法适应设备时,受影响的边缘会出现黄色和黑色条纹图案。在此示例中,视口宽 400 像素,每个子级宽 150 像素。

通过使用 Expanded
widget,可以调整 widget 大小以适应行或列。为了解决上一个示例中图像行对于其渲染盒来说过宽的问题,请将每个图像包装在一个 Expanded
widget 中。
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
widget 还可以指定一个 widget 相对于其同级应占用多少空间。例如,你可能希望某个 widget 占用其同级两倍的空间。为此,请使用 Expanded
widget 的 flex
属性,这是一个整数,用于确定 widget 的弹性因子。默认弹性因子为 1。以下代码将中间图像的弹性因子设置为 2
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),
),
],
);
}

DevTools 和调试布局
#在某些情况下,盒的约束是无限制的,或者说是无限的。这意味着最大宽度或最大高度被设置为 double.infinity
。当给定无限制约束时,试图尽可能大的盒将无法正常工作,并且在调试模式下会抛出异常。
渲染盒最终出现无限制约束的最常见情况是在弹性盒(Row
或 Column
)内,以及在可滚动区域(例如 ListView
和其他 ScrollView
子类)内。例如,ListView
试图扩展以适应其交叉方向上的可用空间(也许它是一个垂直滚动的块,并试图与其父级一样宽)。如果你将一个垂直滚动的 ListView
嵌套在一个水平滚动的 ListView
内部,则内部列表将尝试尽可能宽,这将是无限宽的,因为外部列表在该方向上是可滚动的。
在构建 Flutter 应用程序时,你可能会遇到的最常见错误是由于不正确使用布局 widget 造成的,该错误被称为“无限制约束”错误。
如果你在首次开始构建 Flutter 应用程序时只准备面对一种错误类型,那就是这种错误。
可滚动 widget
#Flutter 拥有许多可自动滚动的内置 widget,还提供了多种可自定义的 widget,用于创建特定的滚动行为。在此页面上,你将看到如何使用最常见的 widget 来使任何页面可滚动,以及用于创建可滚动列表的 widget。
ListView
#ListView
是一个类似列的 widget,当其内容超出渲染盒时会自动提供滚动。使用 ListView
的最基本方式与使用 Column
或 Row
非常相似。与列或行不同,ListView
要求其子级占用交叉轴上的所有可用空间,如下面的示例所示。
Widget build(BuildContext context) {
return ListView(
children: const [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}

当列表项数量未知或非常大(或无限)时,通常使用 ListView
。在这种情况下,最好使用 ListView.builder
构造函数。builder 构造函数只构建当前屏幕上可见的子级。
在以下示例中,ListView
正在显示待办事项列表。待办事项是从存储库中获取的,因此待办事项的数量是未知的。
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),
],
),
);
},
);
}

自适应布局
#由于 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。
在以下示例中,LayoutBuilder
返回的 widget 会根据视口是否小于等于 600 像素或大于 600 像素而改变。
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
构造函数的示例
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 中。
为了说明这一点,以下示例更改了每隔一个列表项的背景颜色。
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),
],
),
);
},
);
}

额外资源
#- 常见布局 widget 和概念
- 调整 widget 大小和定位
- 可滚动 widget
- 示例代码:处理长列表
- 示例代码:创建水平列表
- 示例代码:创建网格列表
- 视频:ListView—Flutter 每周 Widget
- 自适应应用
API 参考
#以下资源解释了各个 API。
反馈
#由于本网站的此部分正在不断完善中,我们欢迎你的反馈!