【Flutter 高级动效】打造磁性粒子流交互动画

719 阅读5分钟

前言

在现代应用界面中,流畅的动画效果往往能提升用户体验。本文将详细介绍如何在Flutter中实现一个具有"磁性"效果的粒子流动画 - 当用户触摸屏幕时,粒子会被吸引并形成流动的视觉效果。这个效果不仅视觉上令人惊艳,还能提供有趣的用户交互体验。

SimulatorScreenRecording-iPhone15-2025-04-08at10.57.41-ezgif.com-video-to-gif-converter.gif

技术原理

这个效果的核心是通过以下几个关键技术实现的:

  1. 自定义绘制(CustomPainter):利用Flutter的CustomPainter实现粒子的绘制和连线
  2. 物理模拟:为每个粒子添加物理属性(位置、速度、加速度)
  3. 交互响应:检测触摸位置并动态调整粒子行为
  4. 帧动画:使用Ticker实现高帧率的连续动画更新

代码实现

1. 基础结构搭建

首先,我们需要创建基本的页面结构:

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);
  
  // 其他初始化代码...
}

注意这里我们使用了SingleTickerProviderStateMixin,这是为了获取Ticker实例,它能提供比AnimationController更精细的帧控制。

2. 粒子类定义

为了简化主逻辑,我们创建一个专门的Particle类来管理每个粒子的属性:

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);
  }
}

每个粒子包含位置、速度、大小、不透明度等多种属性,还特别保存了一个原始位置originalPosition,这是为了实现粒子的"回弹"效果。

3. 粒子初始化逻辑

在初始化阶段,我们需要随机生成多个具有不同属性的粒子:

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(),
      ),
    );
  }
}

这段代码生成了120个不同的粒子,每个粒子都有随机的位置、速度、大小、不透明度和颜色。注意我们使用了3种主色来增加视觉多样性。

4. 粒子更新逻辑

粒子的运动是整个效果的核心。每一帧,我们需要更新所有粒子的状态:

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. 边界碰撞检测(确保粒子不会离开屏幕)

5. 自定义绘制

最后也是最关键的部分是使用CustomPainter绘制粒子和连线:

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. 绘制触摸点的光晕效果

性能优化要点

实现这种高帧率动画时,性能优化至关重要:

  1. 使用Ticker而非Timer:Ticker与渲染系统同步,更适合动画
  2. 避免不必要的setState:只在UI需要更新时触发
  3. 限制粒子数量:120个粒子是在视觉效果和性能之间的权衡
  4. 距离判断优化:使用距离平方进行判断,避免开方运算
  5. 连线优化:只绘制距离小于阈值的粒子连线,而非所有组合

实际应用场景

这种磁性粒子效果可以应用于:

  1. 应用启动屏或欢迎页
  2. 数据可视化的交互背景
  3. 游戏中的特效元素
  4. 品牌展示页面的背景

总结

通过Flutter的CustomPainter和物理模拟,我们实现了一个既美观又具有交互性的磁性粒子流效果。核心技术包括:

  • 自定义绘制系统
  • 基础物理模拟(速度、加速度、摩擦力)
  • 交互响应处理
  • 视觉效果优化(发光、连线、透明度变化)

这个例子展示了Flutter在创建复杂视觉效果方面的强大能力,同时也是学习自定义绘制和动画的绝佳案例。

通过分解复杂效果为简单步骤,即使是看似复杂的动画也能被清晰理解和实现。希望这篇文章能帮助大家掌握Flutter高级动画的实现思路。