👨🏻‍💻's 博客

慢品人间烟火色,闲观万事岁月长

0%

Flutter 布局

要点

Flutter 布局的核心机制是 widget。在 Flutter 中,几乎所有东西都是 widget — 甚至布局模型都是 widget。你在 Flutter 应用程序中看到的图像,图标和文本都是 widget。此外不能直接看到的也是 widget,例如用来排列、限制和对齐可见 widget 的行、列和网格。

布局

流程

  1. 选择一个布局Widget
  2. 创建一个可见Widget
  3. 将可见Widget添加到布局Widget
  4. 将布局Widget添加到页面
  5. 运行应用

Center

步骤1: 选择Center Widget

1
2
3
4
5
6
7
8
9
10
11
12
@override
// 步骤4: 一个 Flutter 应用本身就是一个 widget,大多数 widget 都有一个 build() 方法,在应用的 build() 方法中实例化和返回一个 widget 会让它显示出来。
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
// 步骤3: 通过child属性将Text Widget添加到Center Widget中
body: Center(
child: Text('Hello World!'), // 创建可见的 Widget
),
),
);
}

所有布局Widget都具有以下任一项:

  • 一个child属性,当布局Widget只包含一个子项,比如Center,Container
  • 一个children属性,当布局Widget包含多个子项,比如Row,Column,ListViewStack

对应 Material 应用,可以使用 Scaffold widget,它提供默认的 banner 背景颜色,还有用于添加抽屉、提示条和底部列表弹窗的 API

Row(列) , Column(行),Container , spacing, Padding, SizedBox, margin,Expanded

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter布局学习",
home: Scaffold(
appBar: AppBar(title: Text("Flutter 布局学习")),
body: Column(
spacing: 6,
children: [
Column(
spacing: 10,
children: [
Text("回乡偶书", style: TextStyle(fontSize: 24)),
Text("唐.贺知章"),
Text(
"少小离家老大回,乡音无改鬓毛衰。",
style: TextStyle(
color: Colors.black,
fontSize: 16,
height: 1.5, // 控制行高
),
),
Text(
"儿童相见不相识,笑问客从何处来。",
style: TextStyle(color: Colors.black, fontSize: 16),
),
],
),
Column(
spacing: 6,
// Text 设置左对齐
// 由于Text Widget的大小是自动包裹内容的,
// 所以设置Text的Alignment.left不能生效。
// 当布局是Column时,可以设置Column的
// crossAxisAlignment: CrossAxisAlignment.start。
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text("译文:", textAlign: TextAlign.left),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text(
"年少时离乡老年才归家,我的乡音虽未改变,但鬓角的毛发却已经疏落。家乡的儿童们看见我,没有一个认识我。他们笑着询问我:你是从哪里来的呀?",
),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 8, 4),
child: Text("创作背景:"),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 4),
child: SizedBox(
width: 360,
child: Text(
"贺知章在公元744年(天宝三载),辞去朝廷官职,告老返回故乡越州永兴(今浙江萧山),时已八十六岁,这时,距他中年离乡已经很久了,此诗便作于此时。",
),
),
),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 10,
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 0, 0, 0),
// Image.asset方法里设置的图片路径是相对于pubspec.yaml文件
child: Image.asset(
'assets/images/layout_01@3x.png',
width: 160,
alignment: Alignment.center,
),
),
// 设置Text Widget离右侧屏幕距离为16
// 1. Expanded占满了父Widget的剩余空间
// 2. Container size 包装Text Widget
// 3. margin设置控制右边距
Expanded(
child: Column(
spacing: 10,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
// EdgeInsets.only: 仅设置一个方向的间距
margin: EdgeInsets.only(right: 16),
// L:Left,T:Top,R:Right,B:Bottom 通过英文来记忆4参数
// padding: EdgeInsets.fromLTRB(0, 0, 16,0),
padding: EdgeInsets.only(right: 16),
decoration: BoxDecoration(border: Border.all()),
child: Text(
"问题:请问这首诗表达了作者的什么样的感情?",
maxLines: 4,
softWrap: true,
),
),
Container(
margin: EdgeInsets.only(right: 30),
child: Text(
"如果现在是北京时间早上八点整,我飞往巴黎,到达后巴黎当地时间为早上八点整,请问:我的生命相对延长了吗?",
),
),
Text("答:你把表的电池扣了你是不是就不死了?"),
],
),
),
],
),
],
),
),
);
}

2025-05-05 21.54.23.png

名称 说明
Column 垂直布局,可以包含多个子Widget,子Widget从上往下延伸
Row 水平布局,可以包含多个子Widget,子Widget从左往右延伸
Container 包含一个子Widget,一般用来包装显示的Widget,在这里设置宽高,内外边距
Padding 包含一个子Widget,控制内边距
SizedBox 包含一个子Widget,固定大小
Expanded 包含一个子Widget,填充父Widget的剩余空间

GridView && ListView

使用 GridView 将 widget 作为二维列表展示。 GridView 提供两个预制的列表,或者你可以自定义网格。当 GridView 检测到内容太长而无法适应渲染盒时,它就会自动支持滚动。

GridView的特点

  • 在网格中使用widget
  • 当列的内容超出渲染容器时,它会自动支持滚动
  • 创建自定义的网格,或者使用 GridView.count 设置列的数量/ GridView.extent设置单元格最大宽度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Widget _buildGrid() => GridView.extent(
// 单元格最大宽度,因为子Widget是从上往下延伸,所以主轴是上下,交叉轴是左右,子Widget左右方向的是宽度
maxCrossAxisExtent: 150,
// 上下左右内边距
padding: const EdgeInsets.all(4),
// 上下单元格间距
mainAxisSpacing: 4,
// 左右单元格间距
crossAxisSpacing: 4,
children: _buildGridTileList(30),
);

// 函数支持箭头形式的简写,当函数体只有一行表达式时,可以通过=>简写表示返回表达式的值。
// 从0递增到count-1获取assets/images/pic$i.jpg的图标作为GridView的children
// Generates a list of values.
// Creates a list with [length] positions and fills it with values created by calling [generator]
// for each index in the range 0 .. length - 1 in increasing order.
List<Widget> _buildGridTileList(int count) =>
List.generate(count, (i) => Image.asset('assets/images/pic$i.jpg'));

optimized.gif

ListView是一个和Column相似的Widget,当内容大于自己的渲染框时,就会自动支持滚动

ListView的特点

  • 一个用来组织盒子中列表的专用Column
  • 可以水平或者垂直布局
  • 当检测到空间不足时,会自动支持滚动
  • 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
30
31
32
33
Widget _buildList() {
return ListView(
children: [
_tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters),
_tile('The Castro Theater', '429 Castro St', Icons.theaters),
_tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters),
_tile('Roxie Theater', '3117 16th St', Icons.theaters),
_tile(
'United Artists Stonestown Twin',
'501 Buckingham Way',
Icons.theaters,
),
_tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters),
// 分割线
const Divider(),
_tile('K\'s Kitchen', '757 Monterey Blvd', Icons.restaurant),
_tile('Emmy\'s Restaurant', '1923 Ocean Ave', Icons.restaurant),
_tile('Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant),
_tile('La Ciccia', '291 30th St', Icons.restaurant),
],
);
}

ListTile _tile(String title, String subtitle, IconData icon) {
return ListTile(
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
subtitle: Text(subtitle),
leading: Icon(icon, color: Colors.blue[500]),
);
}

2025-05-06 10.50.56.png

里面每一行都是ListTile,它是Material库中专用的行Widget,它可以很轻松的创建一个包含三行文本以及可选的行前和行尾图标的行。 ListTileCard 或者 ListView 中最常用,但是也可以在别处使用。

Stack

Stack布局用于叠加场景,比如图片上添加文字描述

Stack的特点

  • 用于覆盖另一个Widget – 类似栈:后进(渲染)先出(显示在外层)
  • 子列表中的第一个Widget是基础Widget;后面的子项覆盖在基础Widget的顶部
  • Stack的内容是无法滚动的
  • 可以剪切掉超出渲染框的子项

2025-05-06 11.13.36.png

Card

Material 库中的Card包含相关有价值信息,几乎可以由任何Widget组成,但是通常和ListTile一起使用,Card 只有一个子项,这个子项可以是列,行,列表,网格或者其它支持多个子项的Widget。默认情况下,Card的大小是0x0像素。可以使用SizeBox控制Card的大小

Flutter 中,Card 有轻微的圆角和阴影来使它具有 3D 效果。改变 Cardelevation 属性可以控制阴影效果

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
Widget _buildCard() {
return SizedBox(
height: 210,
child: Card(
child: Column(
children: [
ListTile(
title: const Text(
'1625 Main Street',
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: const Text('My City, CA 99984'),
leading: Icon(Icons.restaurant_menu, color: Colors.blue[500]),
),
const Divider(),
ListTile(
title: const Text(
'(408) 555-1212',
style: TextStyle(fontWeight: FontWeight.w500),
),
leading: Icon(Icons.contact_phone, color: Colors.blue[500]),
),
ListTile(
title: const Text('costa@example.com'),
leading: Icon(Icons.contact_mail, color: Colors.blue[500]),
),
],
),
),
);
}

2025-05-06 11.21.21.png

一些收获

CrossAxisAlignment(交叉轴) && MainAxisAlignment(主轴)

MainAxisAlignment(主轴)就是与当前子Widget延伸方向一致的轴,而CrossAxisAlignment(交叉轴)就是与当前子Widget延伸方向垂直的轴

比如Column: 子Widget是从上往下延伸的,所以主轴就是垂直方向,交叉轴就是水平方向的。
ColumnMainAxisAlignment 属性用于控制子Widget在主轴(垂直轴)上的对齐方式,ColumnCrossAxisAlignment 属性用于控制子Widget在交叉轴(水平轴)上的对齐方式

当设置为CrossAxisAlignment.start时表示Column中的元素左对齐。

格式化

通过快捷键 ⌥(option) + ⇧(shift) + f 可以快速对代码格式化

内容不可见

Flutter界面会告警提示当前的布局超过多少像素,存在内容不可见的问题

Waiting for another flutter command to release the startup lock

问题: VS Code 创建新的Flutter工程一直卡在Waiting for another flutter command to release the startup lock

方案:

1
2
# 执行后关闭VS Code 重新打开再次创建Flutter工程
killall -9 dart

ERR : Proxy failed to establish tunnel (503 Forwarding failure), uri=//pub.flutter-io.cn:443

一般是网络原因,我是终端加了端口转发,然后一直访问不了pub.flutter-io.cn,所以去掉代理

1
2
3
# 去掉代理 + 重新执行 flutter pub get --verbose
export https_proxy=
export http_proxy=

参考

  1. Waiting for another flutter command to release the startup lock
  2. flutter 设置Text居左无效
  3. Flutter 中的 Column 小部件:全面指南
  4. https://docs.flutter.cn/ui/layout/constraints/
  5. GridView.extent中maxCrossAxisExtent的作用