Flutter 中的布局
概述
#Flutter 布局机制的核心是 widget。在 Flutter 中,几乎所有东西都是 widget——甚至布局模型也是 widget。你在 Flutter 应用中看到的图像、图标和文本都是 widget。但你没有看到的东西也是 widget,例如用于排列、约束和对齐可见 widget 的行、列和网格。你通过组合 widget 来构建更复杂的 widget,从而创建布局。
概念示例
#在以下示例中,第一张截图显示了带有标签的三个图标,第二张截图包含了行和列的视觉布局。在第二张截图中,debugPaintSizeEnabled
设置为 true
,这样你就可以看到视觉布局。


这是上一个示例的 widget 树图

大部分内容应该和你预期的一样,但你可能想知道容器(显示为粉红色)是什么。Container
是一个 widget 类,允许你自定义其子 widget。当你想要添加内边距、外边距、边框或背景颜色等功能时,可以使用 Container
。
每个 Text
widget 都放置在一个 Container
中以添加外边距。整个 Row
也放置在一个 Container
中以在行周围添加内边距。
UI 的其余部分由属性控制。使用 color
属性设置 Icon
的颜色。使用 Text.style
属性设置字体、颜色、字重等。列和行都有属性,允许你指定其子项如何垂直或水平对齐,以及子项应占据多少空间。
布局 widget
#如何在 Flutter 中布局单个 widget?本节向你展示如何创建和显示一个简单的 widget。它还显示了一个简单的 Hello World 应用程序的完整代码。
在 Flutter 中,在屏幕上放置文本、图标或图像只需要几个步骤。
1. 选择布局 widget
#根据你希望如何对齐或约束可见 widget 来选择各种布局 widget,因为这些特性通常会传递给包含的 widget。
例如,你可以使用 Center
布局 widget 将可见 widget 水平和垂直居中。
Center(
// Content to be centered here.
)
2. 创建可见 widget
#为你的应用选择一个可见 widget,以包含可见元素,例如文本、图像或图标。
例如,你可以使用 Text
widget 来显示一些文本
Text('Hello World')
3. 将可见 widget 添加到布局 widget 中
#所有布局 widget 都有以下其中之一:
- 如果它们只接受一个子项,则为
child
属性——例如Center
或Container
- 如果它们接受一个 widget 列表,则为
children
属性——例如Row
、Column
、ListView
或Stack
。
将 Text
widget 添加到 Center
widget 中
const Center(
child: Text('Hello World'),
),
4. 将布局 widget 添加到页面中
#Flutter 应用程序本身就是一个 widget,大多数 widget 都有一个 build()
方法。在应用程序的 build()
方法中实例化并返回一个 widget 将显示该 widget。
对于一个通用应用,你可以将 Container
widget 添加到应用的 build()
方法中
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
widget;它提供默认的横幅、背景颜色,并提供用于添加抽屉、SnackBar 和底部工作表的 API。然后你可以将 Center
widget 直接添加到主页的 body
属性中。
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
应用,请使用 CupertinoApp
和 CupertinoPageScaffold
widget。
与 Material
不同,它不提供默认横幅或背景颜色。你需要自己设置这些。
要设置默认颜色,请将配置好的
CupertinoThemeData
传递给应用的theme
属性。要在应用的顶部添加 iOS 风格的导航栏,请将
CupertinoNavigationBar
widget 添加到脚手架的navigationBar
属性中。你可以使用CupertinoColors
提供的颜色来配置你的 widget,使其与 iOS 设计相匹配。要布局你的应用程序主体,请将脚手架的
child
属性设置为所需的 widget,例如Center
或Column
。
要了解可以添加哪些其他 UI 组件,请查看 Cupertino 库。
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. 运行你的应用
#
垂直和水平布局多个 widget
#最常见的布局模式之一是垂直或水平排列 widget。你可以使用 Row
widget 水平排列 widget,使用 Column
widget 垂直排列 widget。
要在 Flutter 中创建行或列,你需要将子 widget 列表添加到 Row
或 Column
widget 中。反过来,每个子项本身可以是行或列,依此类推。以下示例显示了如何在行或列内部嵌套行或列。
此布局组织为 Row
。该行包含两个子项:左侧是一个列,右侧是一个图像。

左侧列的 widget 树嵌套了行和列。

你将在嵌套行和列中实现 Pavlova 的部分布局代码。
对齐 widget
#你可以使用 mainAxisAlignment
和 crossAxisAlignment
属性控制行或列如何对齐其子项。对于行,主轴水平运行,交叉轴垂直运行。对于列,主轴垂直运行,交叉轴水平运行。


MainAxisAlignment
和 CrossAxisAlignment
枚举提供了各种常量来控制对齐方式。
在以下示例中,3 张图像中的每张都宽 100 像素。渲染框(在本例中为整个屏幕)宽于 300 像素,因此将主轴对齐方式设置为 spaceEvenly
会在每张图像之间、之前和之后均匀分配水平自由空间。
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),
Image.asset('images/pic2.jpg'),
Image.asset('images/pic3.jpg'),
],
);

应用源代码: row_column
列的工作方式与行相同。以下示例显示了一列 3 张图像,每张高 100 像素。渲染框的高度(在本例中为整个屏幕)大于 300 像素,因此将主轴对齐方式设置为 spaceEvenly
会在每张图像之间、上方和下方均匀分配垂直自由空间。
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),
Image.asset('images/pic2.jpg'),
Image.asset('images/pic3.jpg'),
],
);

应用源代码: row_column
设置 widget 大小
#当布局太大无法适应设备时,受影响的边缘会出现黄黑相间的条纹图案。这是一个行太宽的示例

可以使用 Expanded
widget 来调整 widget 的大小,使其适应行或列。要修复上一个示例中图像行太宽而无法适应其渲染框的问题,请将每个图像用 Expanded
widget 包装起来。
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')),
],
);

应用源代码: sizing
也许你希望一个 widget 占据其同级两倍的空间。为此,请使用 Expanded
widget 的 flex
属性,这是一个整数,用于确定 widget 的 flex 因子。默认的 flex 因子为 1。以下代码将中间图像的 flex 因子设置为 2
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')),
],
);

应用源代码: sizing
紧凑排列 widget
#默认情况下,行或列会尽可能占据其主轴上的空间,但如果你想将子项紧密地排列在一起,请将其 mainAxisSize
设置为 MainAxisSize.min
。以下示例使用此属性将星形图标紧密地排列在一起。
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),
],
)

应用源代码: pavlova
嵌套行和列
#布局框架允许你根据需要将行和列嵌套在行和列中。让我们看看以下布局中带轮廓部分的的代码

带轮廓的部分实现为两行。评分行包含五颗星和评论数量。图标行包含三列图标和文本。
评分行的 widget 树

ratings
变量创建了一个行,其中包含一个较小的 5 星图标行和文本
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 列;每列包含一个图标和两行文本,你可以在其 widget 树中看到

iconList
变量定义了图标行
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 的标题和文本
final leftColumn = Container(
padding: const EdgeInsets.fromLTRB(20, 30, 20, 20),
child: Column(children: [titleText, subTitle, ratings, iconList]),
);
左侧列放置在 SizedBox
中以约束其宽度。最后,UI 是通过将整个行(包含左侧列和图像)放置在 Card
中来构建的。
Pavlova 图像来自 Pixabay。你可以使用 Image.network()
嵌入来自网络的图像,但在此示例中,图像保存到项目中的 images 目录,添加到 pubspec 文件中,并使用 Images.asset()
访问。有关更多信息,请参阅添加资源和图像。
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
常用布局 widget
#Flutter 拥有丰富的布局 widget 库。以下是其中最常用的一些。目的是让你尽快上手,而不是用完整的列表让你不知所措。有关其他可用 widget 的信息,请参阅Widget 目录,或使用API 参考文档中的搜索框。此外,API 文档中的 widget 页面通常会提供有关可能更适合你需求的类似 widget 的建议。
以下 widget 分为两类:widget 库中的标准 widget,以及 Material 库中的专用 widget。任何应用都可以使用 widget 库,但只有 Material 应用才能使用 Material Components 库。
CupertinoPageScaffold
- 为 iOS 风格的页面提供基本布局结构。
CupertinoNavigationBar
- 在屏幕顶部创建 iOS 风格的导航栏。
CupertinoSegmentedControl
- 创建用于选择的分段控件。
CupertinoTabBar
和CupertinoTabScaffold
- 创建 iOS 特有的底部标签栏。
容器
#许多布局大量使用 Container
来使用内边距分隔 widget,或添加边框或外边距。你可以通过将整个布局放入 Container
中并更改其背景颜色或图像来更改设备的背景。
Container 摘要
#- 添加内边距、外边距、边框
- 更改背景颜色或图像
- 包含单个子 widget,但该子 widget 可以是
Row
、Column
,甚至是 widget 树的根

Container 示例
#此布局由两行组成,每行包含 2 张图像。使用 Container
将列的背景颜色更改为较浅的灰色。
Widget _buildImageColumn() {
return Container(
decoration: const BoxDecoration(color: Colors.black26),
child: Column(children: [_buildImageRow(1), _buildImageRow(3)]),
);
}

Container
还用于为每个图像添加圆角边框和外边距
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
将 widget 布局为二维列表。GridView
提供了两个预制的列表,你也可以构建自己的自定义网格。当 GridView
检测到其内容太长而无法适应渲染框时,它会自动滚动。
GridView 摘要
#- 在网格中布局 widget
- 检测到列内容超出渲染框时自动提供滚动
- 构建你自己的自定义网格,或使用提供的网格之一
GridView.count
允许你指定列数GridView.extent
允许你指定瓷砖的最大像素宽度
GridView 示例
#
使用 GridView.count
创建一个网格,在纵向模式下宽 2 个瓷砖,在横向模式下宽 3 个瓷砖。通过设置每个 GridTile
的 footer
属性来创建标题。
Dart 代码: grid_list_demo.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
是一种类似列的 widget,当其内容太长而无法适应其渲染框时,它会自动提供滚动。
ListView 摘要
#- 一种专门的
Column
,用于组织箱体列表 - 可以水平或垂直布局
- 检测到内容不适合时提供滚动
- 比
Column
配置性低,但更易于使用并支持滚动
ListView 示例
#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
将 widget 排列在基础 widget(通常是图像)之上。widget 可以完全或部分覆盖基础 widget。
Stack 摘要
#- 用于重叠其他 widget 的 widget
- 子列表中的第一个 widget 是基础 widget;随后的子项会覆盖在该基础 widget 上
Stack
的内容无法滚动- 你可以选择裁剪超出渲染框的子项
Stack 示例
#
使用 Stack
在 CircleAvatar
之上叠加一个 Container
(它在半透明黑色背景上显示其 Text
)。Stack
使用 alignment
属性和 Alignment
s 偏移文本。
应用源代码: card_and_stack
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
#Card
,来自 Material 库,包含相关信息片段,可以由几乎任何 widget 组成,但通常与 ListTile
一起使用。Card
只有一个子项,但其子项可以是列、行、列表、网格或其他支持多个子项的 widget。默认情况下,Card
将其大小缩小到 0 乘 0 像素。你可以使用 SizedBox
来约束卡片的大小。
在 Flutter 中,Card
具有略微圆角和阴影,使其具有 3D 效果。更改 Card
的 elevation
属性可以控制阴影效果。例如,将高程设置为 24 会使 Card
在视觉上从表面进一步抬起,并使阴影更加分散。有关支持的高程值列表,请参阅 Material 指南中的高程。指定不支持的值会完全禁用阴影。
Card 摘要
#- 实现 Material 卡片
- 用于呈现相关信息片段
- 接受单个子项,但该子项可以是
Row
、Column
或其他包含子项列表的 widget - 显示时带有圆角和阴影
Card
的内容无法滚动- 来自 Material 库
Card 示例
#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 库的专用行 widget,可以轻松创建一个包含最多 3 行文本以及可选的前导和尾随图标的行。ListTile
最常用于 Card
或 ListView
,但也可以用于其他地方。
ListTile 摘要
#- 一种专用行,包含最多 3 行文本和可选图标
- 比
Row
配置性低,但更易于使用 - 来自 Material 库
ListTile 示例
#约束
#要完全理解 Flutter 的布局系统,你需要了解 Flutter 如何定位和调整布局中组件的大小。有关更多信息,请参阅理解约束。
视频
#以下视频是 Flutter in Focus 系列的一部分,解释了 Stateless
和 Stateful
widget。
Widget of the Week 系列的每一集都专注于一个 widget。其中一些包括布局 widget。
Flutter Widget of the Week 播放列表
其他资源
#以下资源可能有助于编写布局代码。
- 布局教程
- 学习如何构建布局。
- 组件目录
- 描述了 Flutter 中许多可用的 widget。
- Flutter 中的 HTML/CSS 类比
- 对于熟悉 Web 编程的人,此页面将 HTML/CSS 功能映射到 Flutter 功能。
- API 参考文档
- 所有 Flutter 库的参考文档。
- 添加资源和图像
- 解释如何将图像和其他资源添加到你的应用程序包中。
- 从零到一使用 Flutter
- 一个人编写第一个 Flutter 应用程序的经验。