构建 Flutter 布局
本教程解释了如何在 Flutter 中设计和构建布局。
如果你使用提供的示例代码,你可以构建以下应用。
照片由 Dino Reichmuth 在 Unsplash 上拍摄。文字由 瑞士旅游局 提供。
要更好地了解布局机制,请从 Flutter 的布局方法 开始。
绘制布局图
#在本节中,考虑一下你希望为你的应用用户提供哪种类型的用户体验。
考虑如何定位用户界面组件。布局包含这些定位的最终结果。考虑规划你的布局以加快你的编码速度。使用视觉提示来了解屏幕上某个东西的位置可以提供很大的帮助。
使用你喜欢的任何方法,例如界面设计工具或铅笔和一张纸。在编写代码之前,先确定要将元素放置在屏幕上的哪个位置。这是编程版的格言:“量两次,切一次”。
提出以下问题,将布局分解为其基本元素。
- 你能识别出行和列吗?
- 布局是否包含网格?
- 是否有重叠的元素?
- UI 是否需要标签?
- 你需要对齐、填充或边框哪些内容?
识别较大的元素。在本例中,你将图像、标题、按钮和描述排列成一列。
绘制每个行的示意图。
第 1 行,即**标题**部分,有三个子元素:一个文本列、一个星形图标和一个数字。它的第一个子元素,即列,包含两行文本。该第一列可能需要更多空间。
第 2 行,即**按钮**部分,有三个子元素:每个子元素包含一个列,然后包含一个图标和文本。
绘制布局示意图后,考虑如何对其进行编码。
你会将所有代码都写入一个类中吗?或者,你会为布局的每个部分创建一个类吗?
为了遵循 Flutter 的最佳实践,创建一个类或小部件来包含布局的每个部分。当 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
,文本行会在单词边界处换行之前填充列宽度。
更新应用以显示文本部分
#添加一个新的 TextSection
小部件作为 ButtonSection
之后的子元素。添加 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 的最新稳定版本。页面上次更新于 2024-09-26。 查看源代码 或 报告问题。