【譯】帶有Flutter的粒子動畫

原文連接:medium.com/@felixblasc…git

在上一篇《【譯】Flutter中的花式背景動畫》 中介紹瞭如何使用 simple_animations 快速實現一個漂亮的背景動畫,此次將向你展現另一種使用 simple_animations 建立的漂亮粒子動畫的實現。github

動畫由一個漸變的背景,而且有大量的氣泡從底部到頂部升起,而後顯示一些文本內容。canvas

氣泡

這個動畫最有趣的部分就是氣泡,我在這個粒子動畫系統中使用了大約有 30 個氣泡,建立氣泡時它會在底部選擇一個隨機的起始位置,在頂部選擇一個隨機的目標位置,而且它也具備隨機大小以及隨機速度bash

若是氣泡到達頂部,它將再次被隨機的各類屬性從新建立,看起來像這樣:app

粒子模型

以下代碼所示是咱們粒子模型的 dart 代碼:less

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 函數中,咱們定義了粒子在屏幕中的開始位置和結束位置;dom

  • 對於 y 值,是 0.0 表示頂部,1.0 表示在底部,而且 1.2 是比底部低 20%。;
  • 對於 x 值,其表示類似。

而後這個過程當中氣泡的位置和大小是隨機的,咱們使用這些位置來建立補間動畫的值,這裏使用了 simple_animationsMultiTrackTween 來支持一次插入多個補間屬性(x,y)。ide

咱們但願 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 基於時間的緣由,結果將以下所示:

看起來還不錯,但這裏有一個問題,因爲全部 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 項目登陸頁的效果:

資源推薦

相關文章
相關標籤/搜索