Flutter 中的布局
Flutter 布局机制的核心是小部件。在 Flutter 中,几乎所有内容都是小部件,甚至布局模型也是小部件。您在 Flutter 应用中看到的图像、图标和文本都是小部件。但您看不到的内容也是小部件,例如排列、约束和对齐可见小部件的行、列和网格。
通过组合小部件来构建更复杂的小部件,从而创建布局。例如,下面的第一个屏幕截图显示了 3 个图标,每个图标下面都有一个标签
第二个屏幕截图显示了可视布局,显示了 3 列一行,其中每列包含一个图标和一个标签。
以下是此 UI 的小组件树的图表
其中大部分内容应符合您的预期,但您可能想知道容器(以粉红色显示)。Container
是一个允许您自定义其子小组件的小组件类。当您想添加内边距、外边距、边框或背景颜色(仅举几项功能)时,请使用 Container
。
在此示例中,每个 Text
小组件都放置在 Container
中以添加外边距。整个 Row
也放置在 Container
中以在行周围添加内边距。
此示例中的其余 UI 由属性控制。使用 color
属性设置 Icon
的颜色。使用 Text.style
属性设置字体、颜色、粗细等。列和行具有允许您指定其子元素如何垂直或水平对齐以及子元素应占据多少空间的属性。
布局小组件
如何在 Flutter 中布局单个小组件?本部分将向您展示如何创建和显示简单小组件。它还展示了简单 Hello World 应用程序的完整代码。
在 Flutter 中,只需几个步骤即可在屏幕上放置文本、图标或图像。
1. 选择布局小组件
根据您希望如何对齐或约束可见小组件,从各种 布局小组件 中进行选择,因为这些特性通常会传递给所包含的小组件。
此示例使用 Center
,它将内容水平和垂直居中。
2. 创建可见小部件
例如,创建一个 Text
小部件
Text('Hello World'),
创建一个 Image
小部件
return Image.asset(
image,
fit: BoxFit.cover,
);
创建一个 Icon
小部件
Icon(
Icons.star,
color: Colors.red[500],
),
3. 将可见小部件添加到布局小部件
所有布局小部件都具有以下任一属性
- 如果它们采用单个子元素,则具有
child
属性,例如Center
或Container
- 如果它们采用小部件列表,则具有
children
属性,例如Row
、Column
、ListView
或Stack
。
将 Text
小部件添加到 Center
小部件
const Center(
child: Text('Hello World'),
),
4. 将布局小部件添加到页面
Flutter 应用本身是一个小部件,大多数小部件都有一个 build()
方法。在应用的 build()
方法中实例化并返回一个小部件会显示该小部件。
Material 应用
对于 Material
应用,可以使用 Scaffold
小部件;它提供默认横幅、背景颜色,并具有用于添加抽屉、小吃栏和底部工作表的 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 应用
要创建 Cupertino
应用,请使用 CupertinoApp
和 CupertinoPageScaffold
小组件。
与 Material
不同,它不提供默认横幅或背景颜色。你需要自己设置这些内容。
- 要设置默认颜色,请将已配置的
CupertinoThemeData
传递到应用的theme
属性。 -
要在应用顶部添加 iOS 风格的导航栏,请将
CupertinoNavigationBar
小组件添加到脚手架的navigationBar
属性。你可以使用CupertinoColors
提供的颜色来配置小组件,以匹配 iOS 设计。 - 要布局应用的主体,请将脚手架的
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: <Widget>[
Text('Hello World'),
],
),
),
),
);
}
}
非 Material 应用
对于非 Material 应用,你可以将 Center
小组件添加到应用的 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,
),
),
),
);
}
}
默认情况下,非 Material 应用不包含 AppBar
、标题或背景色。如果你想在非 Material 应用中使用这些功能,你必须自己构建它们。此应用将背景色更改为白色,将文本更改为深灰色,以模仿 Material 应用。
垂直和水平排列多个小部件
最常见的布局模式之一是垂直或水平排列小部件。你可以使用 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
中以限制其宽度。最后,UI 使用包含左列和图像的整个行在 Card
中构建。
Pavlova 图像 来自 Pixabay。你可以使用 Image.network()
从网络嵌入图像,但对于此示例,图像被保存到项目中的图像目录中,添加到 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 库。
标准小部件
-
Container
:为小部件添加填充、边距、边框、背景颜色或其他装饰。 -
GridView
:将小部件布局为可滚动的网格。 -
ListView
:将小部件布局为可滚动的列表。 -
Stack
:将一个小部件叠加在另一个小部件之上。
Material 小部件
Container
许多布局大量使用 Container
来使用填充分隔小部件,或添加边框或边距。你可以通过将整个布局放入 Container
并更改其背景颜色或图像来更改设备的背景。
摘要 (Container)
- 添加填充、边距、边框
- 更改背景颜色或图像
- 包含一个子小部件,但该子小部件可以是行、列,甚至是小部件树的根
示例 (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.extent
创建一个网格,其中图块最大宽度为 150 像素。
应用源代码: grid_and_list
使用 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<Container> _buildGridTileList(int count) => List.generate(
count, (i) => Container(child: Image.asset('images/pic$i.jpg')));
ListView
ListView
,一个类似于列的小组件,当其内容过长而无法容纳其渲染框时,会自动提供滚动。
摘要 (ListView)
- 用于组织框列表的专门
Column
- 可以水平或垂直排列
- 检测到其内容不合适时提供滚动
- 可配置性低于
Column
,但更易于使用且支持滚动
示例 (ListView)
使用 ListView
使用 ListTile
显示商家列表。一个 Divider
将影院与餐厅分开。
应用源代码: grid_and_list
使用 ListView
显示 Colors
,这些颜色来自 Material 2 Design 调色板 的特定颜色系列。
Dart 代码: colors_demo.dart
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)
使用 Stack
将一个 Container
(在半透明黑色背景上显示其 Text
)叠加在一个 CircleAvatar
之上。Stack
使用 alignment
属性和 Alignment
偏移文本。
应用源代码: card_and_stack
使用 Stack
将一个图标叠加在图像之上。
Dart 代码: bottom_navigation_demo.dart
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
会将大小缩小到 0 x 0 像素。您可以使用 SizedBox
来限制卡片的大小。
在 Flutter 中, Card
具有略微圆润的边角和阴影,从而产生 3D 效果。更改 Card
的 elevation
属性可以控制阴影效果。例如,将海拔高度设置为 24,在视觉上会将 Card
从表面进一步提升,并导致阴影变得更加分散。有关受支持的海拔高度值列表,请参阅 Material 指南 中的 海拔高度。指定不受支持的值会完全禁用阴影。
摘要(卡片)
- 实现 Material 卡片
- 用于呈现相关的信息片段
- 接受单个子项,但该子项可以是
Row
、Column
或其他包含子项列表的小部件 - 显示为圆角和阴影
Card
的内容无法滚动- 来自 Material 库
示例(卡片)
Card
包含 3 个 ListTiles,并通过用 SizedBox
包装来调整大小。 Divider
分隔第一个和第二个 ListTiles
。
应用源代码: card_and_stack
Card
包含图像和文本。
Dart 代码: cards_demo.dart
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('[email protected]'),
leading: Icon(
Icons.contact_mail,
color: Colors.blue[500],
),
),
],
),
),
);
}
ListTile
使用 ListTile
(来自 Material 库 的一种专门的行小部件),可以轻松创建包含最多 3 行文本以及可选的前导和尾随图标的行。 ListTile
最常用于 Card
或 ListView
中,但也可以在其他地方使用。
摘要(ListTile)
- 包含最多 3 行文本和可选图标的专门行
- 可配置性低于
Row
,但更易于使用 - 来自 Material 库
示例(ListTile)
包含 3 个 ListTile
的 Card
。
应用源代码: card_and_stack
使用带有前置小组件的 ListTile
。
Dart 代码: list_demo.dart
约束
要完全理解 Flutter 的布局系统,您需要了解 Flutter 如何对布局中的组件进行定位和调整大小。有关更多信息,请参阅 了解约束。
视频
以下视频是 Flutter in Focus 系列的一部分,解释了 Stateless
和 Stateful
小组件。
每集 Widget of the Week 系列 都重点介绍一个小组件。其中几个包括布局小组件。
Flutter Widget of the Week 播放列表
其他资源
以下资源可能有助于编写布局代码。
-
- 布局教程
- 了解如何构建布局。
-
- 小部件目录
- 描述了 Flutter 中提供的许多小组件。
-
- Flutter 中的 HTML/CSS 类似物
- 对于熟悉 Web 编程的人来说,此页面将 HTML/CSS 功能映射到 Flutter 功能。
-
- API 参考文档
- 所有 Flutter 库的参考文档。
-
- 添加资产和图像
- 解释如何将图像和其他资产添加到应用的包中。
-
- 使用 Flutter 从零到一
- 一个人编写其第一个 Flutter 应用的经验。