构建 Flutter 布局
本教程介绍如何在 Flutter 中设计和构建布局。
如果你使用提供的示例代码,则可以构建以下应用。
照片由 Dino Reichmuth 在 Unsplash 上提供。文本由 瑞士旅游局 提供。
要更好地了解布局机制,请从 Flutter 的布局方法 开始。
绘制布局图
在本部分中,考虑你希望为应用用户提供哪种类型的用户体验。
考虑如何定位用户界面的组件。布局由这些定位的最终结果组成。考虑规划布局以加快编码速度。使用视觉提示来了解屏幕上某项内容的位置会很有帮助。
使用你喜欢的任何方法,比如界面设计工具或铅笔和一张纸。在编写代码之前,先确定要在屏幕上放置元素的位置。这是格言“测量两次,切割一次”的编程版本。
-
提出这些问题,将布局分解为基本元素。
- 你能识别出行和列吗?
- 布局是否包含网格?
- 是否有重叠元素?
- UI 是否需要选项卡?
- 你需要对齐、填充或设置边框的是什么?
-
识别较大的元素。在此示例中,你将图像、标题、按钮和描述排列成一列。
-
对每一行绘制图表。
-
第 1 行,即标题部分,有三个子元素:一列文本、一个星形图标和一个数字。其第一个子元素(列)包含两行文本。第一列可能需要更多空间。
-
第 2 行,即按钮部分,有三个子元素:每个子元素都包含一列,然后一列包含一个图标和文本。
-
在绘制布局图表后,考虑如何对其进行编码。
你是否会将所有代码都写在一个类中?或者,你是否会为布局的每个部分创建一个类?
要遵循 Flutter 最佳实践,请创建一个类或小部件来包含布局的每个部分。当 Flutter 需要重新渲染 UI 的一部分时,它会更新更改的最小部分。这就是 Flutter 使“所有内容都成为小部件”的原因。如果仅在 Text
小部件中更改文本,则 Flutter 仅重新绘制该文本。Flutter 会根据用户输入尽可能最少地更改 UI。
对于本教程,将你识别的每个元素写成它自己的小部件。
创建应用基础代码
在本部分中,剥离基本的 Flutter 应用代码以启动你的应用。
-
使用以下代码替换
lib/main.dart
的内容。此应用使用应用标题和应用appBar
上显示的标题的参数。此决策简化了代码。lib/main.dart(全部)import '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
属性中,用 SingleChildScrollView
小组件替换 Center
小组件。在 SingleChildScrollView
小组件中,用 Column
小组件替换 Text
小组件。
@@ -21,2 +17,3 @@
|
|
21
|
- body: const
|
22
|
- child:
|
17
|
+ body: const SingleChildScrollView(
|
18
|
+ child: Column(
|
19
|
+ children: [
|
这些代码更新以以下方式更改应用。
SingleChildScrollView
小组件可以滚动。这允许显示不适合当前屏幕的元素。- 一个
Column
小组件会按其children
属性中列出的顺序显示其内的任何元素。在children
列表中列出的第一个元素会显示在列表顶部。children
列表中的元素会按数组顺序从上到下显示在屏幕上。
更新应用以显示标题部分
将 TitleSection
小组件添加为 children
列表中的第一个元素。这会将其置于屏幕顶部。将提供的名称和位置传递给 TitleSection
构造函数。
@@ -23 +19,6 @@
|
|
19
|
+ children: [
|
20
|
+ TitleSection(
|
21
|
+ name: 'Oeschinen Lake Campground',
|
22
|
+ location: 'Kandersteg, Switzerland',
|
23
|
+
),
|
24
|
+
],
|
添加按钮部分
在此部分中,添加将为您的应用添加功能的按钮。
按钮部分包含三列,它们使用相同的布局:一行文本上的一个图标。
计划将这些列分布在一行中,以便每列占用相同数量的空间。使用主色绘制所有文本和图标。
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
小组件。为了帮助分隔这些子项,一个 Padding
小组件与一个 Padding
小组件一起包装 Text
小组件。
在 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
列表中。
@@ -5,6 +5,7 @@
|
|
5
5
|
name: 'Oeschinen Lake Campground',
|
6
6
|
location: 'Kandersteg, Switzerland',
|
7
7
|
),
|
8
|
+ ButtonSection(),
|
8
9
|
],
|
9
10
|
),
|
10
11
|
),
|
添加文本部分
在此部分中,向此应用添加文本描述。
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
属性设置为位置说明的文本。
@@ -6,6 +6,16 @@
|
|
6
6
|
location: 'Kandersteg, Switzerland',
|
7
7
|
),
|
8
8
|
ButtonSection(),
|
9
|
+ TextSection(
|
10
|
+ description:
|
11
|
+ 'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
|
12
|
+ 'Bernese Alps. Situated 1,578 meters above sea level, it '
|
13
|
+ 'is one of the larger Alpine Lakes. A gondola ride from '
|
14
|
+ 'Kandersteg, followed by a half-hour walk through pastures '
|
15
|
+ 'and pine forest, leads you to the lake, which warms to 20 '
|
16
|
+ 'degrees Celsius in the summer. Activities enjoyed here '
|
17
|
+ 'include rowing, and riding the summer toboggan run.',
|
18
|
+
),
|
9
19
|
],
|
10
20
|
),
|
11
21
|
),
|
添加图片部分
在此部分中,添加图像文件以完成布局。
配置应用以使用提供的图像
要配置应用以引用图像,请修改其 pubspec.yaml
文件。
-
在项目顶部创建一个
images
目录。 -
下载
lake.jpg
图像,并将其添加到新的images
目录中。 -
要包含图片,请在应用根目录下的
pubspec.yaml
文件中添加assets
标记。添加assets
时,它作为代码可用的图片指针集。{step4 → step5}/pubspec.yaml@@ -19,3 +19,5 @@1919flutter:2020uses-material-design: true21+ assets:22+ - 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 以两个约束显示图片。首先,尽可能小地显示图片。其次,覆盖布局分配的所有空间,称为渲染框。
更新应用以显示图片部分
在 children
列表中添加 ImageSection
窗口小部件作为第一个子项。将 image
属性设置为在 配置应用以使用提供的图片 中添加的图片路径。
@@ -1,6 +1,9 @@
|
|
1
1
|
body: const SingleChildScrollView(
|
2
2
|
child: Column(
|
3
3
|
children: [
|
4
|
+ ImageSection(
|
5
|
+ image: 'images/lake.jpg',
|
6
|
+
),
|
4
7
|
TitleSection(
|
5
8
|
name: 'Oeschinen Lake Campground',
|
6
9
|
location: 'Kandersteg, Switzerland',
|
恭喜
就是这样!当你热重载应用时,你的应用看起来应该像这样。
资源
你可以从以下位置访问本教程中使用的资源
Dart 代码: main.dart
图片: ch-photo
Pubspec: pubspec.yaml
下一步
要为这个布局添加交互性,请按照交互性教程操作。