🔗 【Flutter 高级动效】打造磁性粒子流交互动画
🔗 前言
在现代应用界面中,流畅的动画效果往往能提升用户体验。本文将详细介绍如何在Flutter中实现一个具有"磁性"效果的粒子流动画 - 当用户触摸屏幕时,粒子会被吸引并形成流动的视觉效果。这个效果不仅视觉上令人惊艳,还能提供有趣的用户交互体验。
🔗 技术原理
这个效果的核心是通过以下几个关键技术实现的:
- 自定义绘制(CustomPainter):利用Flutter的CustomPainter实现粒子的绘制和连线
- 物理模拟:为每个粒子添加物理属性(位置、速度、加速度)
- 交互响应:检测触摸位置并动态调整粒子行为
- 帧动画:使用Ticker实现高帧率的连续动画更新
🔗 代码实现
🔗 基础结构搭建
首先,我们需要创建基本的页面结构:
dart
class MagneticParticlesPage extends StatefulWidget {
@override
_MagneticParticlesPageState createState() => _MagneticParticlesPageState();
}
class _MagneticParticlesPageState extends State<MagneticParticlesPage>
with SingleTickerProviderStateMixin {
final int particleCount = 120;
final List<Particle> particles = [];
Offset? pointerPosition;
late Ticker _ticker;
double _time = 0;
// 颜色配置
final Color primaryColor = Color(0xFF007AFF);
final Color secondaryColor = Color(0xFF6C13FF);
final Color tertiaryColor = Color(0xFF00E5FF);
// 其他初始化代码...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意这里我们使用了SingleTickerProviderStateMixin,这是为了获取Ticker实例,它能提供比AnimationController更精细的帧控制。
🔗 粒子类定义
为了简化主逻辑,我们创建一个专门的Particle类来管理每个粒子的属性:
dart
class Particle {
Offset position;
Offset velocity;
final double size;
final double opacity;
final Color color;
final Offset originalPosition;
double currentSize;
double currentOpacity;
Particle({
required this.position,
required this.velocity,
required this.size,
required this.opacity,
required this.color,
required this.originalPosition,
}) : currentSize = size,
currentOpacity = opacity;
}
// 为Offset类添加扩展方法,方便向量计算
extension OffsetExtension on Offset {
Offset normalize() {
final magnitude = distance;
if (magnitude == 0) return Offset.zero;
return this / magnitude;
}
Offset clone() {
return Offset(dx, dy);
}
}
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
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
每个粒子包含位置、速度、大小、不透明度等多种属性,还特别保存了一个原始位置originalPosition,这是为了实现粒子的"回弹"效果。
🔗 粒子初始化逻辑
在初始化阶段,我们需要随机生成多个具有不同属性的粒子:
dart
void _initializeParticles() {
final random = Random();
// 生成随机粒子
for (int i = 0; i < particleCount; i++) {
final position = Offset(
random.nextDouble() * _screenSize.width,
random.nextDouble() * _screenSize.height,
);
// 随机速度
final velocity = Offset(
(random.nextDouble() - 0.5) * 1.0,
(random.nextDouble() - 0.5) * 1.0,
);
// 随机大小和不透明度
final size = random.nextDouble() * 2.5 + 1.5;
final opacity = random.nextDouble() * 0.4 + 0.6;
// 随机颜色 - 在三种主色之间选择
Color color;
final colorPick = random.nextDouble();
if (colorPick < 0.33) {
color = primaryColor;
} else if (colorPick < 0.66) {
color = secondaryColor;
} else {
color = tertiaryColor;
}
// 创建粒子并添加到列表
particles.add(
Particle(
position: position,
velocity: velocity,
size: size,
opacity: opacity,
color: color,
originalPosition: position.clone(),
),
);
}
}
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
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
这段代码生成了120个不同的粒子,每个粒子都有随机的位置、速度、大小、不透明度和颜色。注意我们使用了3种主色来增加视觉多样性。
🔗 粒子更新逻辑
粒子的运动是整个效果的核心。每一帧,我们需要更新所有粒子的状态:
dart
void _updateParticles() {
for (final particle in particles) {
// 基础移动 - 微小的随机运动
final noise = Offset(
sin(_time * 0.5 + particle.originalPosition.dx * 0.05) * 0.3,
cos(_time * 0.5 + particle.originalPosition.dy * 0.05) * 0.3,
);
// 更新位置
particle.position = particle.position + particle.velocity + noise;
// 慢慢将粒子拉回其原始位置附近
final toOriginal = particle.originalPosition - particle.position;
particle.velocity = particle.velocity + toOriginal * 0.003;
// 摩擦力,减慢粒子速度
particle.velocity = particle.velocity * 0.95;
// 如果有鼠标/触摸点,添加吸引力
if (pointerPosition != null) {
final pointerForce = pointerPosition! - particle.position;
final distance = pointerForce.distance;
// 吸引距离范围
final attractRadius = 180.0;
if (distance < attractRadius) {
// 吸引力随距离减弱
final strength = 1.0 - (distance / attractRadius);
final attractForce = pointerForce.normalize() * strength * 2.0;
// 应用吸引力
particle.velocity = particle.velocity + attractForce;
// 接近鼠标时增加不透明度和大小
particle.currentOpacity = particle.opacity + strength * 0.3;
particle.currentSize = particle.size + strength * 2.0;
} else {
// 恢复正常状态
particle.currentOpacity = particle.opacity;
particle.currentSize = particle.size;
}
} else {
// 无鼠标/触摸时恢复正常状态
particle.currentOpacity = particle.opacity;
particle.currentSize = particle.size;
}
// 边界检查 - 保持粒子在屏幕内
// ...边界检查代码...
}
}
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
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
这段逻辑包含了几个关键的物理模拟:
- 基础的随机运动(使用正弦余弦函数产生平滑的伪随机位移)
- 弹簧力(将粒子拉回原始位置)
- 摩擦力(减缓粒子运动,提供阻尼效果)
- 磁性吸引力(当有触摸时,粒子会被吸引)
- 边界碰撞检测(确保粒子不会离开屏幕)
🔗 自定义绘制
最后也是最关键的部分是使用CustomPainter绘制粒子和连线:
dart
class ParticlePainter extends CustomPainter {
final List<Particle> particles;
final Offset? pointerPosition;
final linkDistance = 100.0;
ParticlePainter({
required this.particles,
this.pointerPosition,
});
@override
void paint(Canvas canvas, Size size) {
// 绘制粒子连线
final linePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0.5;
// 检查粒子距离并绘制连线
for (int i = 0; i < particles.length; i++) {
final p1 = particles[i];
// 绘制鼠标/触摸与附近粒子的连线
if (pointerPosition != null) {
final pointerDistance = (p1.position - pointerPosition!).distance;
if (pointerDistance < linkDistance) {
final opacity = (1.0 - pointerDistance / linkDistance) * 0.8;
linePaint.color = p1.color.withOpacity(opacity * 0.5);
canvas.drawLine(pointerPosition!, p1.position, linePaint);
}
}
// 绘制粒子之间的连线
for (int j = i + 1; j < particles.length; j++) {
final p2 = particles[j];
final distance = (p1.position - p2.position).distance;
if (distance < linkDistance) {
final opacity = (1.0 - distance / linkDistance) * 0.3;
final blendedColor = Color.lerp(p1.color, p2.color, 0.5)!;
linePaint.color = blendedColor.withOpacity(opacity);
canvas.drawLine(p1.position, p2.position, linePaint);
}
}
}
// 绘制粒子本身
for (final particle in particles) {
final paint = Paint()
..color = particle.color.withOpacity(particle.currentOpacity)
..style = PaintingStyle.fill
..maskFilter =
MaskFilter.blur(BlurStyle.normal, particle.currentSize * 0.5);
// 绘制粒子
canvas.drawCircle(particle.position, particle.currentSize, paint);
// 绘制发光核心
final corePaint = Paint()
..color = Colors.white.withOpacity(particle.currentOpacity * 0.7)
..style = PaintingStyle.fill;
canvas.drawCircle(
particle.position, particle.currentSize * 0.4, corePaint);
}
// 绘制触摸点光晕效果
if (pointerPosition != null) {
// ...光晕效果绘制代码...
}
}
@override
bool shouldRepaint(covariant ParticlePainter oldDelegate) {
return true; // 每帧都重绘
}
}
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
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
绘制过程分为三个主要部分:
- 绘制粒子之间以及粒子与触摸点之间的连线
- 绘制粒子本身,包括发光效果
- 绘制触摸点的光晕效果
🔗 性能优化要点
实现这种高帧率动画时,性能优化至关重要:
- 使用Ticker而非Timer:Ticker与渲染系统同步,更适合动画
- 避免不必要的setState:只在UI需要更新时触发
- 限制粒子数量:120个粒子是在视觉效果和性能之间的权衡
- 距离判断优化:使用距离平方进行判断,避免开方运算
- 连线优化:只绘制距离小于阈值的粒子连线,而非所有组合
🔗 实际应用场景
这种磁性粒子效果可以应用于:
- 应用启动屏或欢迎页
- 数据可视化的交互背景
- 游戏中的特效元素
- 品牌展示页面的背景
🔗 总结
通过Flutter的CustomPainter和物理模拟,我们实现了一个既美观又具有交互性的磁性粒子流效果。核心技术包括:
- 自定义绘制系统
- 基础物理模拟(速度、加速度、摩擦力)
- 交互响应处理
- 视觉效果优化(发光、连线、透明度变化)