以下是 Flutter 官方网站构建布局的例子, 最终创建以下的布局页面:

新建 Flutter 项目

安装和配置好 Flutter 开发环境之后, 完成以下的步骤:

  1. 创建一个新的 Flutter 应用

  2. 项目中新建文件 lib\views\home_view.dart, 编辑内容如下:

1
2


  1. 编辑 lib/main.dart 文件的内容如下:
1

  1. Android 模拟器中调试运行项目

对布局进行图形分解

第一步需要将布局分解成它的各个基础元素:

  • 识别出它的行和列。

  • 这个布局是否包含网格布局?

  • 是否有重叠的元素?

  • 界面是否需要选项卡?

  • 留意需要对齐、内间距、或者边界的区域。

首先,识别出稍大的元素。在这个例子中,四个元素排成一列:一个图像,两个行区域,和一个文本区域。

接着,对每一行进行图解。

第一行: 图片显示区域

第二行: 也就是标题区域, 有三个子元素:一个文本列, 一个星形图标, 和一个数字。它的第一个子元素,文本列,包含两行文本。第一列占据大量空间,因此它应当被封装在一个 Expanded widget 当中。

第三行: 也就是按钮区域, 同样有三个子元素:每个子元素是一个包含图标和文本的列。

第四行: 文本区域, 显示长段文本内容

一旦图解好布局,采取自下而上的方法来实现它就变得尤为轻松了。为了最大程度减少,深层嵌套的布局代码带来的视觉混乱,需要用一些变量和函数来替代某些实现。

实现图片区域

图片组件是Flutter基础组件之一,和文本组件一样必不可少,

使用网络图片

1
2
3
Image.network(
'https://media-cdn.tripadvisor.com/media/photo-s/0c/92/17/86/st-mary-lake-glacier.jpg',
)

使用本地图片

更新 pubspec.yaml 文件,添加一个 assets 标签。这使得在你的代码中可以访问到该图片。

1
2
3
4
flutter:            
uses-material-design: true
assets:
- images/lake.jpg

请注意,pubspec.yaml 区分大小写,因此请编写如上所示的 assets: 和图像 URL

pubspec 文件对空格也很敏感,因此请使用适当的缩进。

你可能需要重新启动正在运行的程序(在模拟器或连接的设备上)才能使 pubspec 更改生效。

1
2
3
4
5
6
7
8
body: Column(            
children: [
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
),

实现标题行

首先,构建标题部分左侧列。添加如下代码到 HomeView 类的 build() (与 return Scaffold(); 并列)方法内顶部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Container(
padding: const EdgeInsets.only(bottom: 8),
child: const Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
/*3*/
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
],
),
);

Column 元素放到 Expanded widget 中可以拉伸该列,以利用该行中所有剩余的闲置空间。设置 crossAxisAlignment 属性值为 CrossAxisAlignment.start,这会将该列放置在行的起始位置。

将第一行文本放入 Container 容器中使得你可以增加内间距。列中的第二个子元素,同样为文本,显示为灰色。

标题行中的最后两项是一个红色星形图标,和文字41。整行都在一个 Container 容器布局中,而且每条边都有 32 像素的内间距。

添加标题部分到body 中, 修改home_view.dart 中的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
return MaterialApp(            
title: 'Flutter layout demo',
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter layout demo'),
),
body: Column(
children: [
titleSection,
],
),
),
);

实现按钮行

按钮区域包含三列使用相同布局-一行文本上面一个图标。此行的各列被等间隙放置,文本和图标被着以初始色。

由于构建每列的代码基本相同,因此可以创建一个名为 _buildButtonColumn() 的私有辅助函数,以颜色、图标和文本为入参,返回一个以指定颜色绘制自身 widgets 的一个 column 列对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class HomeView extends StatelessWidget {
const HomeView({super.key});

@override
Widget build(BuildContext context) {
// ···
}

Column _buildButtonColumn(Color color, IconData icon, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
}

这个函数直接将图标添加到这列里。文本在以一个仅有上间距的 Container 容器中,使得文本与图标分隔开。

通过调用函数并传递针对某列的颜色Icon 图标和文本,来构建包含这些列的行。然后在行的主轴方向通过使用 MainAxisAlignment.spaceEvenly,将剩余的空间均分到每列各自的前后及中间。只需在 build() 方法中的 titleSection 声明下添加如下代码:

1
2
3
4
5
6
7
8
9
10
Color color = Theme.of(context).primaryColor;

Widget buttonSection = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildButtonColumn(color, Icons.call, 'CALL'),
_buildButtonColumn(color, Icons.near_me, 'ROUTE'),
_buildButtonColumn(color, Icons.share, 'SHARE'),
],
);

添加按钮部分到 body 属性中去:

1
2
3
4
5
6
body: Column(            
children: [
titleSection,
buttonSection,
],
),

实现文本区域

将文本区域定义为一个变量,将文本放置到一个 Container 容器中,然后为每条边添加内边距。只需在 buttonSection 声明的下方添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
Widget textSection = const Padding(
padding: EdgeInsets.all(32),
child: Text(
'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.',
softWrap: true,
),
);

通过设置 softwraptrue,文本将在填充满列宽后在单词边界处自动换行

添加文本部分到 body 属性:

1
2
3
4
5
6
7
body: Column(            
children: [
titleSection,
buttonSection,
textSection,
],
),

使用 ListView

在最后的步骤中,需要在一个 ListView 中排列好所有的元素,而不是在一个 Column 中,因为当 app 运行在某个小设备上或当前显示内容超出屏幕高度时,ListView 支持 app body 的滚动。

1
2
3
4
5
6
7
body: ListView(            
children: [
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,

加入交互体验

如何修改您的应用程序以使其对用户输入做出反应?在本教程中,您将为仅包含非交互式 widget 的应用程序添加交互性。具体来说,您将通过创建一个管理两个无状态 widget 的自定义有状态 widget,修改一个图标实现使其可点击.

当应用第一次启动时,这个星形图标是实心红色,表明这个湖以前已经被收藏过了。星号旁边的数字表示 41 个人已经收藏了此湖。完成本教程后,点击星形图标将取消收藏状态,然后用轮廓线的星形图标代替实心的,并减少计数。再次点击会重新收藏,并增加计数。

为了实现这个,您将创建一个包含星形图标和计数的自定义 widget,它们都是 widget。因为点击星形图标会更改这两个 widget 的状态,所以同一个 widget 应该同时管理这两个 widget

有状态和无状态的 widgets

有些 widgets 是有状态的, 有些是无状态的。如果用户与 widget 交互,widget 会发生变化,那么它就是 有状态的。

无状态widget 自身无法改变。 IconIconButtonText 都是无状态 widget,它们都是 StatelessWidget 的子类。

有状态widget 自身是可动态改变的(基于State)。例如,可以通过与用户的交互或是随着数据改变而导致外观形态的变化。 CheckboxRadioSliderInkWellFormTextField 都是有状态 widget,它们都是 StatefulWidget 的子类。

一个 widget 的状态保存在一个 State 对象中,它和 widget 的显示分离。 Widget 的状态是一些可以更改的值,如一个滑动条的当前值或一个复选框是否被选中。当 widget 状态改变时,State 对象调用 setState(),告诉框架去重绘 widget