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


这是上一个示例的组件树图

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

左侧列的组件树嵌套了行和列。

你将在嵌套行和列中实现 Pavlova 的部分布局代码。
对齐组件
#你使用 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
调整组件大小
#当布局过大无法适应设备时,受影响的边缘会出现黄黑相间的条纹图案。这是一个行过宽的示例

可以通过使用 Expanded
组件来调整组件大小以适应行或列。为了解决上一个示例中图像行对于其渲染框过宽的问题,请用 Expanded
组件包装每个图像。
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
也许你希望一个组件占用其同级组件两倍的空间。为此,请使用 Expanded
组件的 flex
属性,这是一个整数,用于确定组件的 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
紧凑排列组件
#默认情况下,行或列会沿其主轴占用尽可能多的空间,但如果你想将子项紧密地排列在一起,请将其 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
嵌套行和列
#布局框架允许你根据需要深度嵌套行和列。让我们看看以下布局中带轮廓部分的的代码

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

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 列;每列包含一个图标和两行文本,如其组件树所示

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
中以限制其宽度。最后,使用包含左列和图像的整个行在 Card
内部构建 UI。
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
常用布局组件
#Flutter 拥有丰富的布局组件库。以下是一些最常用的组件。目的是让你尽快上手,而不是用完整的列表让你不知所措。有关其他可用组件的信息,请参阅组件目录,或使用 API 参考文档中的搜索框。此外,API 文档中的组件页面通常会提供关于可能更适合你需求的类似组件的建议。
以下组件分为两类:来自组件库的标准组件,以及来自Material 库的专用组件。任何应用都可以使用组件库,但只有 Material 应用才能使用 Material Components 库。
CupertinoPageScaffold
- 为 iOS 风格的页面提供基本布局结构。
CupertinoNavigationBar
- 在屏幕顶部创建 iOS 风格的导航栏。
CupertinoSegmentedControl
- 创建一个用于选择的分段控件。
CupertinoTabBar
和CupertinoTabScaffold
- 创建 iOS 特有的底部选项卡栏。
容器
#许多布局大量使用 Container
来使用内边距分隔组件,或添加边框或外边距。你可以通过将整个布局放置在 Container
中并更改其背景颜色或图像来更改设备的背景。
总结 (Container)
#- 添加内边距、外边距、边框
- 更改背景颜色或图像
- 包含一个子组件,但该子组件可以是
Row
、Column
,甚至是组件树的根

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