原文连接:medium.com/@felixblasc…git
在上一篇《【译】Flutter中的花式背景动画》 中介绍了如何使用 simple_animations 快速实现一个漂亮的背景动画,此次将向你展现另一种使用 simple_animations 建立的漂亮粒子动画的实现。github
动画由一个渐变的背景,而且有大量的气泡从底部到顶部升起,而后显示一些文本内容。canvas
这个动画最有趣的部分就是气泡,我在这个粒子动画系统中使用了大约有 30 个气泡,建立气泡时它会在底部选择一个随机的起始位置,在顶部选择一个随机的目标位置,而且它也具备随机大小以及随机速度。bash
若是气泡到达顶部,它将再次被随机的各类属性从新建立,看起来像这样:markdown
以下代码所示是咱们粒子模型的 dart 代码:app
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
函数中,咱们定义了粒子在屏幕中的开始位置和结束位置;less
而后这个过程当中气泡的位置和大小是随机的,咱们使用这些位置来建立补间动画的值,这里使用了 simple_animations 的 MultiTrackTween
来支持一次插入多个补间属性(x,y)。dom
咱们但愿 x 的位置和 y 的位置具备不一样的动画效果,而且能够实现一些不错缓慢移动的效果。ide
接着咱们使用 simple_animations 的 AnimationProgress
建立一个对象,用于对补间动画提供的实际进度,这里须要的是一个开始时间和一个持续时间:函数
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
基于时间的缘由,结果将以下所示:
看起来还不错,但这里有一个问题,因为全部 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
函数中,而后再开始动画时,全部气泡从一开始就在屏幕上分布良好:
对于背景渐变在上一篇《【译】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()), ]); } } 复制代码
这是最终结果:
这是应用到 CarGuo/gsy_github_app_flutter 项目登陆页的效果: