今天来学学更多的flutter的组件

弹性布局-Flex

image-20260427103207193

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity, //宽度占满父组件
height: double.infinity, //高度占满父组件
decoration: BoxDecoration(color: Colors.tealAccent),
child: Flex(
direction: Axis.horizontal, //主轴水平
//direction: Axis.vertical, //主轴垂直
children: [
//Expaned组件会占满剩余空间
Expanded(
flex: 1, //flex属性表示占剩余空间的比例,如果有多个Expanded组件,flex值越大占的空间越多
child: Container(
color: Colors.redAccent,
height: 50,
width: 50,
),
),

Expanded(
flex: 2,
child: Container(
color: Colors.blueAccent,
height: 50,
width: 50,
),
),
],
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427105320683

Flexible

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity, //宽度占满父组件
height: double.infinity, //高度占满父组件
decoration: BoxDecoration(color: Colors.tealAccent),
child: Flex(
direction: Axis.horizontal, //主轴水平
//direction: Axis.vertical, //主轴垂直
children: [
//Flexible组件不同于Expanded组件,Flexible组件可以设置flex属性来控制占用剩余空间的比例,而Expanded组件默认占用所有剩余空间
Flexible(
flex: 50, //flex属性表示占剩余空间的比例,如果有多个Flexible组件,flex值越大占的空间越多
child: Container(
color: Colors.redAccent,
height: 100,
width: 100,
),
),

Flexible(
flex: 2,
child: Container(
color: Colors.blueAccent,
height: 100,
width: 100,
),
),
],
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427110008830

Flex布局受其父组件传递的约束影响。确保父组件提供了适当的布局约束

Expanded与Flexible的区别:

——– Expanded强制子组件填满所有剩余空间,

——– Flexible根据自身大小调整,不强制占满空间

我们来练习一下

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity, //宽度占满父组件
height: double.infinity, //高度占满父组件
decoration: BoxDecoration(color: Colors.tealAccent),
child: Flex(
direction: Axis.vertical, //主轴垂直
children: [
Container(color: Colors.redAccent, height: 100),
Expanded(
child: Container(
color: Colors.greenAccent,
width: double.infinity,
height: 100,
),
),
Container(color: Colors.blueAccent, height: 100),
],
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427111112801

流式布局-Wrap

image-20260427111509237

当子组件内容是根据数据动态生成时,使用wrap可以确保布局始终适配

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

//使用Widgt类型的返回值,生成一个包含10个Container组件的列表
List<Widget> getList() {
//List.generate(count, generator)方法可以生成一个包含count个元素的列表,generator是一个函数,用于生成每个元素的值
//这里用(){}匿名函数来生成每个元素的值,index是当前元素的索引,从0开始
return List.generate(13, (index) {
return Container(width: 100, height: 100, color: Colors.redAccent);
});
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity, //宽度占满父组件
height: double.infinity, //高度占满父组件
decoration: BoxDecoration(color: Colors.tealAccent),
child: Wrap(
spacing: 10, //主轴上间距
runSpacing: 10, //交叉轴上间距
alignment: WrapAlignment.spaceAround, //主轴对齐方式为均分
direction: Axis.horizontal, //主轴方向为水平方向
children: getList(), //生成10个子组件
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427113827205

List.generate是一个构造器,用于快速创建长度固定且每个元素可以通过索引号确定的列表语法:

List.generate(int count, E generator(int index), {bool growable: false})

层叠布局-Stack/Positioned

image-20260427152644733

image-20260427160646763

可以看出这个是层叠式的,一层叠一层

image-20260427154016318

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.lightBlueAccent,
child: Stack(
alignment: Alignment.center, //让每一个子组件都居中
children: [
Container(width: 300, height: 300, color: Colors.red),
Container(width: 200, height: 200, color: Colors.green),
Container(width: 100, height: 100, color: Colors.blue),
],
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

Positioned

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.lightBlueAccent,
child: Stack(
alignment: Alignment.center,
children: [
Container(width: 150, height: 150, color: Colors.red),
Container(width: 100, height: 100, color: Colors.green),
Container(width: 50, height: 50, color: Colors.blue),
Positioned(
//Positionzd组件用于确定子组件的位置
right: 50,
bottom: 50,
child: Container(width: 50, height: 50, color: Colors.yellow),
),
Positioned(
left: 50,
bottom: 50,
child: Container(width: 50, height: 50, color: Colors.yellow),
),
Positioned(
right: 50,
top: 50,
child: Container(width: 50, height: 50, color: Colors.yellow),
),
Positioned(
left: 50,
top: 50,
child: Container(width: 50, height: 50, color: Colors.yellow),
),
],
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427155308373

还有一个很奇葩的玩法就是,我们在之前学习的padding这里可以用positioned来代替效果

首先先了解positioned的一个特性,如果我们同时输入上下左右四个方向

1
2
3
4
5
6
7
Positioned(
right: 50,
top: 50,
bottom: 50,
left: 50,
child: Container(width: 50, height: 50, color: Colors.yellow),
),

此时代码不会报错,而是子组件往四个方向延伸来覆盖容器

image-20260427155905606

而且取值为0的时候甚至全部覆盖body

1
2
3
4
5
6
7
Positioned(
right: 0,
top: 0,
bottom: 0,
left: 0,
child: Container(width: 50, height: 50, color: Colors.yellow),
),

image-20260427160250154

同理,我们也可以制造单方向延伸的条

1
2
3
4
5
6
Positioned(
right: 0,
bottom: 0,
left: 0,
child: Container(width: 50, height: 50, color: Colors.yellow),
),

image-20260427160510691

文本组件-Text

image-20260427160906913

image-20260427192614139

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.lightBlueAccent,
alignment: Alignment.center,
child: Text(
"终末地AI群聊",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold, //fontWeight这里是加粗
color: Colors.white, //color是字体颜色
fontStyle: FontStyle.italic, //fontStyle这里是斜体
shadows: [
Shadow(
color: Colors.black45,
offset: Offset(2, 2),
blurRadius: 4,
),
], //shadows是阴影
decoration: TextDecoration.underline, //decoration是下饰线
decorationColor: Colors.white70, //decorationColor是下饰线的颜色
),
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

我们普通的文字排列是默认的换行

image-20260427193608648

但是我们也可以指定显示的行号

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.lightBlueAccent,
alignment: Alignment.center,
child: Text(
"终末地AI群聊:欢迎管理员群主进入!本群的管理者有陈千语干员和佩丽卡总督等人,新成员为庄方宜天师和弥弗长官,请大家遵守群规,友好相处哦!",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold, //fontWeight这里是加粗
color: Colors.white, //color是字体颜色
fontStyle: FontStyle.italic, //fontStyle这里是斜体
shadows: [
Shadow(
color: Colors.black45,
offset: Offset(2, 2),
blurRadius: 4,
),
], //shadows是阴影
decoration: TextDecoration.underline, //decoration是下饰线
decorationColor: Colors.white70, //decorationColor是下饰线的颜色
),
maxLines: 1, //maxLines是文本的最大行数,这里是1行
overflow: TextOverflow.ellipsis, //overflow是文本溢出时的处理方式,这里是用省略号表示溢出部分
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427193807845

TextSpan

如果需要在同一段本中显示不同样式,可用Text.rich构造函数配合TextSpan来实现

image-20260427195400407

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
alignment: Alignment.center,
child: Text.rich(
// 使用 Text.rich 来创建富文本富文本就是可以在同一段文本中使用不同的样式
TextSpan(
text: "点击进入,打开你的拉电线之旅~~\n\n",
children: [
TextSpan(
text: "欢迎来到终末地AI群聊!\n\n",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.cyanAccent,
),
),
TextSpan(
text: "在这里,你可以与各种AI角色进行有趣的对话\n\n",
style: TextStyle(fontSize: 16, color: Colors.indigoAccent),
),
TextSpan(text: "梨花飘落你窗前~~\n\n", style: TextStyle(fontSize: 16)),
],
style: TextStyle(
fontSize: 18,
color: Colors.deepOrangeAccent,
), // 这是默认的样式,如果子 TextSpan 没有指定样式,就会使用这个样式
),
),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427195655702

图片组件-image

image-20260427200101988

image-20260427211656998

image.asset(本地资源)

pubspec.yaml启用图片文件夹

将资源assets功能启用

1
2
3
assets:
- images/a_dot_burr.jpeg
- images/a_dot_ham.jpeg

这个需要一些配置

image-20260427200736889

例如我们将图片放到这个images文件夹

image-20260427201351231

然后在main.dart中启用资源,同时重新run执行

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatelessWidget {
const MainPage({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
alignment: Alignment.center,
child: Image.asset("lib/images/01.png"),
),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text("终末地AI群聊")),
),
),
);
}
}

image-20260427202244242

当然也可以使用width和hight来控制图片的大小

1
child: Image.asset("lib/images/01.png", width: 500, fit: BoxFit.contain),
fit属性

fit属性

这里fit 用的是 BoxFit 这个枚举,一共有 7种,每一种控制图片如何在给定空间里缩放/裁剪

1. BoxFit.fill

1
fit: BoxFit.fill

👉 强行拉伸填满

  • 会变形 ❗(宽高比被破坏)
  • 很少用,除非你不在意比例

2. BoxFit.contain

1
fit: BoxFit.contain

👉 完整显示图片

  • 不裁剪
  • 保持比例
  • 可能有空白(黑边)

📌 类似:图片“缩小装进盒子”


3. BoxFit.cover ⭐(最常用)

1
fit: BoxFit.cover

👉 铺满容器

  • 保持比例
  • 会裁剪超出的部分

📌 类似:背景图常用(推荐)


4. BoxFit.fitWidth

1
fit: BoxFit.fitWidth

👉 宽度填满

  • 高度按比例缩放
  • 可能上下裁剪

5. BoxFit.fitHeight

1
fit: BoxFit.fitHeight

👉 高度填满

  • 宽度按比例缩放
  • 可能左右裁剪

6. BoxFit.none

1
fit: BoxFit.none

👉 不缩放

  • 原始大小显示
  • 超出部分直接裁掉

7. BoxFit.scaleDown

1
fit: BoxFit.scaleDown

👉 只缩小,不放大

  • 类似 contain
  • 但如果图片本来就小,就不会放大

image.network(网络资源)

这个不需要在pubspec.yaml 直接使用就行了,把网址或者库粘贴进去

1
Image.network('https://example.com/image.png')

但是问题是,这个东西需要联网,否则图片会显示不出来, 除此之外, Android/HarmonyOS/iOS使用Image.network需要配置网络权限

而且如果图片的源头断了也不会显示出来

文本输入组件-TextField

image-20260427212237017

image-20260427224300913

这里我们可以写一个登录界面,使用有状态组件StatefulWidget(因为这个是动态刷新的)

image-20260427222355749

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatefulWidget {
const MainPage({super.key});

@override
State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
TextEditingController _usernameController =
TextEditingController(); // 实例化一个用户名输入控制器
TextEditingController _passwordController =
TextEditingController(); // 实例化一个密码输入控制器

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Container(
padding: EdgeInsets.all(20),
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, //界面元素垂直居中

children: [
TextField(
controller: _usernameController, // 绑定用户名输入控制器
onChanged: (value) => print("用户名输入: $value"), // 用户名输入时的回调
onSubmitted: (value) => print("用户名提交: $value"), // 用户名提交时的回调
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
left: 20,
), // 输入框内容内边距,这里设置为左侧20像素
hintText: "请输入用户名", // 输入框提示文本
fillColor: const Color.fromARGB(255, 85, 242, 227), // 输入框背景颜色
filled: true, // 是否填充背景颜色
labelText: "用户名", // 输入框标签
border: OutlineInputBorder(
// 输入框边框样式,这里设置为无边框并且圆角
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(25),
),
),
),
SizedBox(height: 10), // 输入框之间的间距
TextField(
controller: _passwordController, // 绑定密码输入控制器
obscureText: true, // 隐藏输入内容,适用于密码输入
onChanged: (value) => print("密码输入: $value"), // 密码输入时的回调
onSubmitted: (value) => print("密码提交: $value"), // 密码提交时的回调
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
left: 20,
), // 输入框内容内边距,这里设置为左侧20像素
hintText: "请输入密码", // 输入框提示文本
fillColor: const Color.fromARGB(255, 85, 242, 227), // 输入框背景颜色
filled: true, // 是否填充背景颜色
labelText: "密码", // 输入框标签
border: OutlineInputBorder(
// 输入框边框样式,这里设置为无边框并且圆角
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(25),
),
),
),
SizedBox(height: 10), // 输入框和按钮之间的间距
Container(
height: 50,
width: double.infinity,
decoration: BoxDecoration(
// 按钮的装饰,这里设置为黑色背景和圆角
color: Colors.black,
borderRadius: BorderRadius.circular(25),
),
child: TextButton(
// 登录按钮,使用TextButton组件,并且设置按钮文本颜色为白色
onPressed: () {
print(
"用户名: ${_usernameController.text}, 密码: ${_passwordController.text}",
);
},
child: Text("登录", style: TextStyle(color: Colors.white)),
),
),
],
),
),
bottomNavigationBar: BottomAppBar(
child: Container(
height: 50.0,
child: Center(child: Text("© 2026 终末地AI群聊")),
),
),
),
);
}
}

常用滚动组件

image-20260427224457489

SingleChildScrollView

image-20260428131617737

包裹个组件,让单个组件具备滚动能

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
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatefulWidget {
const MainPage({super.key});

@override
State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
TextEditingController _usernameController =
TextEditingController(); // 用户名输入控制器
TextEditingController _passwordController =
TextEditingController(); // 密码输入控制器

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: SingleChildScrollView(
child: Column(
children: List.generate(100, (index) {
return Container(
margin: EdgeInsets.only(top: 10),
width: double.infinity,
color: Colors.lightBlueAccent,
height: 100,
child: Text(
"这是第${index + 1}条消息",
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
alignment: Alignment.center,
);
}),
),
),
bottomNavigationBar: BottomAppBar(
child: Container(
height: 50.0,
child: Center(child: Text("© 2026 终末地AI群聊")),
),
),
),
);
}
}

然后我们可以增加一个回到首页的按钮

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import 'package:flutter/material.dart';

void main() {
runApp(MainPage());
}

class MainPage extends StatefulWidget {
const MainPage({super.key});

@override
State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
ScrollController _scrollController = ScrollController(); //滚动控制器

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "终末地AI群聊",
home: Scaffold(
appBar: AppBar(centerTitle: true, title: Text("终末地AI群聊")),
body: Stack(
// 使用Stack布局,允许内容滚动
children: [
SingleChildScrollView(
controller: _scrollController, //添加滚动控制器
padding: EdgeInsets.all(20), // 添加内边距
child: Column(
children: List.generate(100, (index) {
return Container(
margin: EdgeInsets.only(top: 10),
width: double.infinity,
color: Colors.lightBlueAccent,
height: 100,
child: Text(
"这是第${index + 1}条消息",
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
alignment: Alignment.center,
);
}),
),
),
Positioned(
right: 10,
bottom: 10,
child: GestureDetector(
onTap: () {
print("回到顶部按钮已被点击");
//_scrollController.jumpTo(0);
_scrollController.animateTo(
0,
duration: Duration(seconds: 1),
curve: Curves.ease,
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: Colors.cyanAccent,
),

width: 70,
height: 70,
alignment: Alignment.center,
child: Text(
"回到顶部",
style: TextStyle(
fontSize: 15,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
Positioned(
right: 10,
top: 10,
child: GestureDetector(
onTap: () {
print("回到底部按钮已被点击");
// _scrollController.jumpTo(
// _scrollController.position.maxScrollExtent,
// );
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(seconds: 1),
curve: Curves.ease,
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: Colors.cyanAccent,
),

width: 70,
height: 70,
alignment: Alignment.center,
child: Text(
"回到底部",
style: TextStyle(
fontSize: 15,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),

bottomNavigationBar: BottomAppBar(
child: Container(
height: 50.0,
child: Center(child: Text("© 2026 终末地AI群聊")),
),
),
),
);
}
}

image-20260428093647708

其中这里面涉及的两个方法—jumpTo和animateTo这里细说一下

jumpTo(直接跳转)

1
_scrollController.jumpTo(0);

本质:瞬间跳转到指定滚动位置(没有动画)

参数只有一个

含义
0 顶部
_scrollController.position.maxScrollExtent 底部
任意数字 指定位置

特点

  • 不会有任何过渡效果
  • 立即生效
  • 用户体验比较“生硬”
  • 适合:
    • 初始化位置
    • 强制修正滚动位置
    • 不关心动画的场景

举例

1
2
3
_scrollController.jumpTo(
_scrollController.position.maxScrollExtent,
);

效果就是: 一点击直接“啪”一下到底,没有滚动过程

animateTo(动画平滑)

1
2
3
4
5
animateTo(
double offset, {
required Duration duration,
required Curve curve,
})
第一个参数offset(滚动位置)
含义
0 顶部
_scrollController.position.maxScrollExtent 底部
任意数字 指定位置

这个和jumpTo是一样的

第二个参数duration(动画时长)
1
Duration(seconds: 1)

即动画持续时间为1秒

你可以这样写:

1
2
Duration(milliseconds: 300)
Duration(seconds: 2)

效果:

  • 时间越短 → 越快
  • 时间越长 → 越慢
第三个参数curves(动画曲线)
1
Curves.bounceIn

控制动画“运动方式”(非常重要)

常见曲线:

曲线 效果
Curves.linear 匀速(最死板)
Curves.ease 平滑(推荐)
Curves.easeIn 先慢后快
Curves.easeOut 先快后慢
Curves.easeInOut 两头慢中间快
Curves.bounceIn 弹跳进入(你现在用的)
Curves.elasticOut 弹簧效果

ListView

image-20260430163411866