29

【译】带有Flutter的粒子动画

 4 years ago
source link: https://juejin.im/post/5e1d5b9b6fb9a03013306588
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2020年01月15日 阅读 5594

【译】带有Flutter的粒子动画

原文链接:medium.com/@felixblasc…

在上一篇《【译】Flutter中的花式背景动画》 中介绍了如何使用 simple_animations 快速实现一个漂亮的背景动画,这次将向你展示另外一种使用 simple_animations 创建的漂亮粒子动画的实现。

16fa2ae692b40c3b?imageslim

动画由一个渐变的背景,并且有大量的气泡从底部到顶部升起,然后显示一些文本内容。

这个动画最有趣的部分就是气泡,我在这个粒子动画系统中使用了大约有 30 个气泡,创建气泡时它会在底部选择一个随机的起始位置,在顶部选择一个随机的目标位置,并且它也具有随机大小以及随机速度

如果气泡到达顶部,它将再次被随机的各种属性重新创建,看起来像这样:

16fa2aeba6bfb000?imageslim

如下代码所示是我们粒子模型的 dart 代码:

class ParticleModel {
  Animatable tween;
  double size;
  AnimationProgress animationProgress;
  Random random;

  ParticleModel(this.random) {
    restart();
  }

  restart({Duration time = Duration.zero}) {
    final startPosition = Offset(-0.2 + 1.4 * random.nextDouble(), 1.2);
    final endPosition = Offset(-0.2 + 1.4 * random.nextDouble(), -0.2);
    final duration = Duration(milliseconds: 500 + random.nextInt(1000));

    tween = MultiTrackTween([
      Track("x").add(
          duration, Tween(begin: startPosition.dx, end: endPosition.dx),
          curve: Curves.easeInOutSine),
      Track("y").add(
          duration, Tween(begin: startPosition.dy, end: endPosition.dy),
          curve: Curves.easeIn),
    ]);
    animationProgress = AnimationProgress(duration: duration, startTime: time);
    size = 0.2 + random.nextDouble() * 0.4;
  }

  maintainRestart(Duration time) {
    if (animationProgress.progress(time) == 1.0) {
      restart(time: time);
    }
  }
}
复制代码

如上代码可以看到,在这里我们传入了一个随机生成器,然后执行了 restart 粒子的动画,在 restart 函数中,我们定义了粒子在屏幕中的开始位置和结束位置;

  • 对于 y 值,是 0.0 表示顶部,1.0 表示在底部,并且 1.2 是比底部低 20%。;
  • 对于 x 值,其表示相似。

然后这个过程中气泡的位置和大小是随机的,我们使用这些位置来创建补间动画的值,这里使用了 simple_animationsMultiTrackTween 来支持一次插入多个补间属性(x,y)。

我们希望 x 的位置和 y 的位置具有不同的动画效果,并且可以实现一些不错缓慢移动的效果。

接着我们使用 simple_animationsAnimationProgress 创建一个对象,用于对补间动画提供的实际进度,这里需要的是一个开始时间和一个持续时间:

  • 这里通过 restart 函数传递开始时间。
  • 对于持续时间我们选择一些随机值 500 + random.nextInt(1000)

最后提供一个 maintainRestart 函数 用于我们外部调用,以检查是否需要重新启动粒子动画,它通过调用 AnimationProgressprogress(time)progress 函数来查询状态。这个值介于 0.0 - 1.0 之间。

前面准备好了动画粒子的生命周期模型之后,现在可以开始绘制它们了,这里我们使用 Flutter 的 CustomPainter 来绘制粒子列表:

class ParticlePainter extends CustomPainter {
  List<ParticleModel> particles;
  Duration time;

  ParticlePainter(this.particles, this.time);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.white.withAlpha(50);

    particles.forEach((particle) {
      var progress = particle.animationProgress.progress(time);
      final animation = particle.tween.transform(progress);
      final position =
          Offset(animation["x"] * size.width, animation["y"] * size.height);
      canvas.drawCircle(position, size.width * 0.2 * particle.size, paint);
    });
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
复制代码

paint 函数中,我们循环列表中的所有粒子并查询其进度值, 然后将这些进度值传递到指定的补间动画中,以获取动画的实际相对位置, 最后我们将它们乘以画布的大小,就可以获得得到需要绘制的绝对位置。

到这里我们完成了粒子模型和绘制,如下代码所示,现在可以创建一个渲染它们的控件:

class Particles extends StatefulWidget {
  final int numberOfParticles;

  Particles(this.numberOfParticles);

  @override
  _ParticlesState createState() => _ParticlesState();
}

class _ParticlesState extends State<Particles> {
  final Random random = Random();

  final List<ParticleModel> particles = [];

  @override
  void initState() {
    List.generate(widget.numberOfParticles, (index) {
      particles.add(ParticleModel(random));
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Rendering(
      builder: (context, time) {
        _simulateParticles(time);
        return CustomPaint(
          painter: ParticlePainter(particles, time),
        );
      },
    );
  }

  _simulateParticles(Duration time) {
    particles.forEach((particle) => particle.maintainRestart(time));
  }
}
复制代码

这里我们创建一个有状态的控件,该控件在初始化时创建了一些粒子模型, 然后在 build函数使用 Rendering 控件(来自simple_animations),该控件会为我们提供的 Painter 和生命周期需要的时间片段。

这个时间从零开始然后实时计数,我们可以利用这段时间来创建固定帧速率的动画,这也是前面 AnimationProgress 基于时间的原因,结果将如下所示:

16fa2b068499fb60?imageslim

看起来还不错,但这里有一个问题,由于所有 30 个粒子在开始时都重新开始,因此会出现屏幕上部没有气泡的情况。

为了解决这个问题,我们需要告诉渲染控件去得到一个不同的开始时间:

@override
Widget build(BuildContext context) {
  return Rendering(
    startTime: Duration(seconds: 30),
    onTick: _simulateParticles,
    builder: (context, time) {
      return CustomPaint(
        painter: ParticlePainter(particles, time),
      );
    },
  );
}
复制代码

我们可以添加一个参数 startTime,该参数将使“ 渲染” 控件可以快速计算出你需要的间隔开始时间,然后我们将粒子动画的起始相关的代码放入 onTick 函数中,然后再开始动画时,所有气泡从一开始就在屏幕上分布良好:

16fa2b133fc42c6e?imageslim

对于背景渐变在上一篇《【译】Flutter中的花式背景动画》 中已经介绍过,这里最后就是将所有控件件都放到 Stack 上:


class ParticleBackgroundApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(children: <Widget>[
      Positioned.fill(child: AnimatedBackground()),
      Positioned.fill(child: Particles(30)),
      Positioned.fill(child: CenteredText()),
    ]);
  }
}
复制代码

这是最终结果:

16fa2b1beb3871b1?imageslim

这是应用到 CarGuo/gsy_github_app_flutter 项目登录页的效果:

16fa2d4a4f65888e?imageslim


1

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK