【译】用户与Flutter交互时的粒子动画

2,597 阅读3分钟

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

依旧是 simple_animations 的漂亮动画应用。

simple_animations 已经完成对 Flutter Web 的支持,并且其背景动画效果(文章) 的演示例子成为了 Flutter Web 的官方示例。

在继《带有Flutter的粒子动画》 之后,今天这一篇将介绍如何使用粒子动画与用户交互的。

如上图所示,动画是一种“廉价”的点击效果,“地鼠”在 6 个不同的地方短暂出现,当单击一个地鼠(“圆”)时,它将分裂成多个颗粒,然后经过一段随机的时间后,它们再次出现,类似一个简单的反应游戏(打地鼠)。

地鼠控件

如下代码所示是一个简化的地鼠控件代码:

class Mole extends StatefulWidget {
  @override
  _MoleState createState() => _MoleState();
}

class _MoleState extends State<Mole> {
  final List<MoleParticle> particles = [];

  bool _moleIsVisible = false;
  
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: _buildMole(),
    );
  }

  Rendering _buildMole() {
    return Rendering(
      onTick: (time) => _manageParticleLifecycle(time),
      builder: (context, time) {
        return Stack(
          overflow: Overflow.visible,
          children: [
            if (_moleIsVisible)
              GestureDetector(onTap: () => _hitMole(time), child: _mole()),
            ...particles.map((it) => it.buildWidget(time))
          ],
        );
      },
    );
  }

  Widget _mole() {
    return Container(
      decoration: BoxDecoration(
          color: Colors.brown, borderRadius: BorderRadius.circular(50)),
    );
  }

  _hitMole(Duration time) {
    _setMoleVisible(false);
    Iterable.generate(50).forEach((i) => particles.add(MoleParticle(time)));
  }

  _manageParticleLifecycle(Duration time) {
    particles.removeWhere((particle) {
      return particle.progress.progress(time) == 1;
    });
  }

}

对于“地鼠”,我们使用 GestureDetector 替换 Container 来将用户的点击放到 _hitMole 方法中,这些“地鼠”与颗粒通过 Stack 堆叠到一起。

在该 _hitMole() 方法的内部,我们隐藏了“地鼠”容器,并生成了 50 个 MoleParticle 粒子,该类还提供了 buildWidget() 用于构建小“地鼠”粒子。

最后通过 Rendering 控件实现绘制,它采用一个 onTick 可以让我们更方便的处理粒子的生命周期,在我们的 _manageParticleLifecycle() 方法中,我们在动画完成后删除了所有粒子。

粒子动画

我们的粒子动画一开始覆盖在原始的“地鼠”容器上,然后随着时间的流逝,粒子容器随机向四周方向飞行直到粒子的容器大小缩减为 0。

如下代码所示, MoleParticle 类实现了这个行为效果:

class MoleParticle {
  Animatable tween;
  AnimationProgress progress;

  MoleParticle(Duration time) {
    final random = Random();
    final x = (100 + 200) * random.nextDouble() * (random.nextBool() ? 1 : -1);
    final y = (100 + 200) * random.nextDouble() * (random.nextBool() ? 1 : -1);

    tween = MultiTrackTween([
      Track("x").add(Duration(seconds: 1), Tween(begin: 0.0, end: x)),
      Track("y").add(Duration(seconds: 1), Tween(begin: 0.0, end: y)),
      Track("scale").add(Duration(seconds: 1), Tween(begin: 1.0, end: 0.0))
    ]);
    progress = AnimationProgress(
        startTime: time, duration: Duration(milliseconds: 600));
  }

  buildWidget(Duration time) {
    final animation = tween.transform(progress.progress(time));
    return Positioned(
      left: animation["x"],
      top: animation["y"],
      child: Transform.scale(
        scale: animation["scale"],
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
              color: Colors.brown, borderRadius: BorderRadius.circular(50)),
        ),
      ),
    );
  }
}

在创建粒子时,我们通过计算出随机目标位置,并将这些信息存储到补间动画里。这里使用了 simple_animationsMultiTrackTween ,因为可以实现一次添加 3 个不同的补间动画属性。

然后这里创建一个 AnimationProgress 用来跟踪动画的进度,我们这里只需要向他提供当前时间即可。

最后通过 buildWidget() 将动画补间值和进度结合 Transform.scalePositioned 来达到移动和缩放的效果。

这里排除了一些重生相关的逻辑,但是您可以在 Simple Animations Example App 中找到完整的演示代码。

这是应用到 CarGuo/gsy_github_app_flutter 项目启动页的效果:

资源推荐