构建 Flutter 布局
学习如何在 Flutter 中构建布局。
本教程介绍如何在 Flutter 中设计和构建布局。
如果您使用提供的示例代码,您可以构建出以下应用。
最终成品应用。
摄影:Dino Reichmuth (来自 Unsplash)。文本:瑞士旅游局。
若要更全面地了解布局机制,请先从 Flutter 的布局方案开始。
绘制布局草图
#在这一部分,请思考您想要为应用用户提供什么样的用户体验。
思考如何定位用户界面的组件。布局即这些定位的总和。考虑提前规划布局以加快编码速度。使用视觉线索来确定屏幕元素的位置会有很大帮助。
无论您喜欢哪种方式(例如界面设计工具或纸笔),请在编写代码之前先确定要在屏幕上放置元素的位置。这就是编程版的谚语:“三思而后行”。
问自己以下问题,将布局拆解为基础元素。
- 你能识别出哪些是行,哪些是列吗?
- 布局是否包含网格?
- 是否有重叠的元素?
- UI 是否需要标签页(tabs)?
- 你需要对齐、添加内边距(padding)或边框的地方在哪里?
识别出较大的元素。在这个示例中,您将图像、标题、按钮和描述排列成一个列(Column)。
布局中的主要元素:图像、行、行和文本块
为每一行绘制图表。
第 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 组件,使其符合以下布局。
标题部分的草图与 UI 原型
添加 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列表中的元素按数组顺序从上到下显示在屏幕上。
更新应用以显示标题部分
#将 TitleSection 组件添加为 children 列表中的第一个元素。这会将其置于屏幕顶部。将提供的名称和位置信息传递给 TitleSection 构造函数。
children: [
TitleSection(
name: 'Oeschinen Lake Campground',
location: 'Kandersteg, Switzerland',
),
],
添加按钮部分
#在这一部分,添加将为您的应用增加功能的按钮。
按钮部分包含三列,它们使用相同的布局:一个图标上方放置一行文本。
按钮部分的草图与 UI 原型
计划将这些列分布在同一行中,以便每个列占用相同的空间。将所有文本和图标设为主题色(primary color)。
添加 ButtonSection 组件
#
在 TitleSection 组件之后添加以下代码,以包含构建按钮行的代码。
class ButtonSection extends StatelessWidget {
const ButtonSection({super.key});
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).primaryColor;
// ···
}
}
创建一个用于生成按钮的组件
#由于每一列的代码可以使用相同的语法,因此创建一个名为 ButtonWithText 的组件。该组件的构造函数接受颜色、图标数据和按钮标签。利用这些值,该组件构建了一个包含 Icon 和样式化的 Text 组件作为其子元素的 Column。为了分隔这些子元素,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(),
],
添加文本部分
#在这一部分,向应用添加文本描述。
文本块的草图与 UI 原型
添加 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 在两个约束条件下显示图像。首先,尽可能显示图像。其次,覆盖布局分配的所有空间,即所谓的渲染框(render box)。
更新应用以显示图像部分
#将 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
后续步骤
#若要为该布局添加交互性,请阅读交互性教程。