构建 Flutter 布局
本教程讲解如何在 Flutter 中设计和构建布局。
如果使用提供的示例代码,您可以构建以下应用。
照片来自 Dino Reichmuth 在 Unsplash 上。 文字来自 瑞士旅游局。
要更好地了解布局机制,请从 Flutter 的布局方法 开始。
绘制布局图
#在本节中,请考虑您希望为应用用户提供哪种类型的用户体验。
考虑如何定位用户界面的组件。 布局由这些位置的最终结果构成。 考虑规划您的布局以加快编码速度。 使用视觉提示来了解屏幕上某个东西的位置会有很大帮助。
使用您喜欢的任何方法,例如界面设计工具或铅笔和一张纸。在编写代码之前,确定您想在屏幕上放置元素的位置。这是格言“量两次,切一次”的编程版本。
提出以下问题以将布局分解为其基本元素。
- 你能识别行和列吗?
- 布局是否包含网格?
- 是否有重叠元素?
- UI 是否需要选项卡?
- 您需要对齐、填充还是边框?
识别较大的元素。在此示例中,您将图像、标题、按钮和说明排列成一列。
绘制每行的图表。
第 1 行( 标题 部分)有三个子项: 文本列、星形图标和数字。 它的第一个子项(列)包含两行文本。 第一个列可能需要更多空间。
第 2 行( 按钮 部分)有三个子项:每个子项包含一个列,然后包含一个图标和文本。
绘制布局图后,考虑您将如何编写代码。
您会将所有代码编写在一个类中吗? 或者,您会为布局的每个部分创建一个类吗?
为了遵循 Flutter 最佳实践,创建一个类或 Widget 来包含布局的每个部分。 当 Flutter 需要重新渲染 UI 的一部分时,它会更新发生变化的最小部分。 这就是 Flutter 使“一切皆为小部件”的原因。 如果 Text
小部件中的只有文本发生更改,Flutter 只会重新绘制该文本。 Flutter 对用户输入的响应是尽可能少地更改 UI。
对于本教程,请将您已识别的每个元素编写为其自己的小部件。
创建应用基本代码
#在本节中,将基本的 Flutter 应用代码分离出来以启动您的应用。
将
lib/main.dart
的内容替换为以下代码。 此应用使用参数作为应用标题和应用appBar
上显示的标题。此决定简化了代码。dartimport 'package:flutter/material.dart'; void main() => runApp(const MyApp()); 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'), ), ), ); } }
添加标题部分
#在本节中,创建一个类似于以下布局的 TitleSection
小部件。
添加 TitleSection
小部件
#在 MyApp
类之后添加以下代码。
class TitleSection extends StatelessWidget {
const TitleSection({
super.key,
required this.name,
required this.location,
});
final String name;
final String location;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
location,
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
/*3*/
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
],
),
);
}
}
- 要使用行中所有剩余的可用空间,请使用
Expanded
小部件来拉伸Column
小部件。 要将列放置在行的开头,请将crossAxisAlignment
属性设置为CrossAxisAlignment.start
。 - 要在文本行之间添加空间,请将这些行放在
Padding
小部件中。 - 标题行以红色星形图标和文本“41”结尾。 整个行位于
Padding
小部件内,并对每个边缘进行 32 像素的填充。
将应用主体更改为可滚动视图
#在 body
属性中,将 Center
小部件替换为 SingleChildScrollView
小部件。 在 SingleChildScrollView
小部件内,将 Text
小部件替换为 Column
小部件。
body: const Center(
child: Text('Hello World'),
body: const SingleChildScrollView(
child: Column(
children: [
这些代码更新会以以下方式更改应用。
SingleChildScrollView
小部件可以滚动。 这允许显示当前屏幕上不适合的元素。Column
小部件按列出的顺序显示其children
属性中的任何元素。children
列表中列出的第一个元素显示在列表顶部。children
列表中的元素在屏幕上从上到下按数组顺序显示。
更新应用以显示标题部分
#将 TitleSection
小部件添加为 children
列表中的第一个元素。 这会将其放置在屏幕顶部。 将提供的名称和位置传递给 TitleSection
构造函数。
children: [
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
],
添加按钮部分
#在本节中,添加将向您的应用添加功能的按钮。
按钮部分包含三个使用相同布局的列: 图标位于文本行上方。
计划在一行中分配这些列,以便每一列占据相同的空间。使用主色绘制所有文本和图标。
添加 ButtonSection
小部件
#在 TitleSection
小部件之后添加以下代码以包含用于构建按钮行的代码。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
// ···
}
}
创建一个用于制作按钮的小部件
#由于每个列的代码可以使用相同的语法,因此创建一个名为 ButtonWithText
的小部件。 该小部件的构造函数接受颜色、图标数据和按钮的标签。 使用这些值,该小部件构建一个 Column
,其中包含 Icon
和一个样式化的 Text
小部件作为其子项。 为了帮助分离这些子项,Text
小部件被一个 Padding
小部件包裹。
在 ButtonSection
类之后添加以下代码。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
// ···
}
class ButtonWithText extends StatelessWidget {
const ButtonWithText({
super.key,
required this.color,
required this.icon,
required this.label,
});
final Color color;
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
使用 Row
小部件定位按钮
#在 ButtonSection
小部件中添加以下代码。
- 为每个按钮添加三个
ButtonWithText
小部件实例。 - 传递该特定按钮的颜色、
Icon
和文本。 - 使用
MainAxisAlignment.spaceEvenly
值沿主轴对齐列。Row
小部件的主轴是水平的,而Column
小部件的主轴是垂直的。 然后,此值告诉 Flutter 在Row
沿每个列之前、之间和之后平均分配可用空间。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
return SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ButtonWithText(
color: color,
icon: Icons.call,
label: 'CALL',
),
ButtonWithText(
color: color,
icon: Icons.near_me,
label: 'ROUTE',
),
ButtonWithText(
color: color,
icon: Icons.share,
label: 'SHARE',
),
],
),
);
}
}
class ButtonWithText extends StatelessWidget {
const ButtonWithText({
super.key,
required this.color,
required this.icon,
required this.label,
});
final Color color;
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Column(
// ···
);
}
}
更新应用以显示按钮部分
#将按钮部分添加到 children
列表中。
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
ButtonSection(),
],
添加文本部分
#在本节中,将文本说明添加到此应用。
添加 TextSection
小部件
#在 ButtonSection
小部件之后,添加以下代码作为单独的小部件。
class TextSection extends StatelessWidget {
const TextSection({
super.key,
required this.description,
});
final String description;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(32),
child: Text(
description,
softWrap: true,
),
);
}
}
通过将 softWrap
设置为 true
,文本行会在单词边界换行之前填充列宽。
更新应用以显示文本部分
#在 ButtonSection
之后添加一个新的 TextSection
小部件作为子项。 添加 TextSection
小部件时,将其 description
属性设置为位置说明的文本。
location: 'Kandersteg, Switzerland',
),
ButtonSection(),
TextSection(
description:
'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
'Bernese Alps. Situated 1,578 meters above sea level, it '
'is one of the larger Alpine Lakes. A gondola ride from '
'Kandersteg, followed by a half-hour walk through pastures '
'and pine forest, leads you to the lake, which warms to 20 '
'degrees Celsius in the summer. Activities enjoyed here '
'include rowing, and riding the summer toboggan run.',
),
],
添加图像部分
#在本节中,添加图像文件以完成您的布局。
配置您的应用以使用提供的图像
#要配置您的应用以引用图像,请修改其 pubspec.yaml
文件。
在项目的顶部创建一个
images
目录。下载
lake.jpg
图像并将其添加到新的images
目录中。要包含图像,请在应用根目录的
pubspec.yaml
文件中添加一个assets
标签。 添加assets
时,它充当指向代码可用的图像的指针集。pubspec.yamlyamlflutter: uses-material-design: true assets: - images/lake.jpg
创建 ImageSection
小部件
#在其他声明之后定义以下 ImageSection
小部件。
class ImageSection extends StatelessWidget {
const ImageSection({super.key, required this.image});
final String image;
@override
Widget build(BuildContext context) {
return Image.asset(
image,
width: 600,
height: 240,
fit: BoxFit.cover,
);
}
}
BoxFit.cover
值告诉 Flutter 以两个约束显示图像。首先,尽可能小地显示图像。其次,覆盖布局分配的所有空间,称为渲染框。
更新应用以显示图像部分
#将 ImageSection
小部件添加为 children
列表中的第一个子项。 将 image
属性设置为在 配置您的应用以使用提供的图像 中添加的图像的路径。
children: [
ImageSection(
image: 'images/lake.jpg',
),
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
恭喜
#就是这样!热重载应用后,您的应用应如下所示。
资源
#您可以从以下位置访问本教程中使用的资源:
Dart 代码: main.dart
图像: ch-photo
Pubspec: pubspec.yaml
下一步
#要向此布局添加交互性,请遵循 交互性教程。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。