AnimationBuilderで作るぴょんぴょん跳ねるWidget

![bounce_icon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/d6fb599f-9f4a-d4f4-cfea-a410fc6c59a5.gif)
自然にユーザの注意を引きたい時、飛び跳ねてアピールするのが有効です
MacOSのDockでも、バックグラウンドで更新されたアプリがぴょんぴょん飛び跳ねることがあります
たくさん使うと目障りですが、ポイントで使うと有効かと思います

今回は、FlutterのAnimationBuilderを使って任意のWidgetをぴょんぴょん跳ねさせるコンテナクラスを作成してみました

# Flutterにおけるアニメーションの種類
[公式ドキュメント](https://flutter.dev/docs/development/ui/animations)で、チャート付きで解説してあります。親切ですね

今回の場合、

* 絵を描くようなアニメーションがしたい？ → いいえ
* テキストアニメーションがしたい？ → いいえ
* アニメーションが常に全面にあってもいいか？ → はい
* アニメーションの値に不連続性はないか？ → はい
* 簡単に、無限に繰り返す必要はないか？ → はい
* 一つの子Widgetだけを動かしたいか？ → はい

と言うことで、TweenAnimationBuilderを使うことにしました

単純に大きくor小さくする、または一方向に動かすようなアニメーションであれば、[AnimatedContainer](https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html)の方が手軽そうです。飛び跳ねる動作の場合は、飛んで、落ちてくる、と言う２アクションが必要なのでAnimatedContainerでは実装が難しそうでした


# ぴょんぴょん飛び跳ねるContainerの実装
と言うことで実装してみたものがこちらです

## ソースコード
```dart:bounce_container.dart
import 'package:flutter/cupertino.dart';

// ========== アニメーション定義 ========== //
// 飛び上がりアニメーション
final _bounceUp = TweenSequenceItem<double>(
  weight: 1,
  tween: Tween(
    begin: 0.0, end: -30.0
  ).chain(CurveTween(curve: Curves.easeOutCubic))
);
// 飛び下がりアニメーション
final _bounceDown = TweenSequenceItem<double>(
  weight: 1,
  tween: Tween(
    begin: -30.0, end: 0.0
  ).chain(CurveTween(curve: Curves.easeInCubic))
);
// 有限回数繰り返すために、TweenSequenceでその分定義する
final _sequence = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      _bounceUp,
      _bounceDown,
      _bounceUp,
      _bounceDown,
      _bounceUp,
      _bounceDown
    ]
);

// ========== Widget定義 ========== //
class BounceContainer extends StatefulWidget {
  final Widget child;
  final state = _BounceContainerState();

  BounceContainer({required this.child});

  @override
  State<StatefulWidget> createState() => state;

  // 外部からアニメーションをトリガーするための公開メソッド
  void bounce() {
    state.animate();
  }
}

// Stateful WidgetかつSingleTickerProviderStateMixinが必要
class _BounceContainerState extends State<BounceContainer> with SingleTickerProviderStateMixin {
  late AnimationController animController;
  late Animation<double> animTween;

  @override
  void initState() {
    super.initState();

    animController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 2000),
    );
    animTween = _sequence.animate(animController);
  }

  @override
  void dispose () {
    animController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: animController,
        builder: (context, child) {
          return Transform.translate(
              offset: Offset(0, animTween.value),
              child: widget.child);
        }
    );
  }

  void animate() {
    animController.reset(); // resetしないと2回目以降に動かない
    animController.forward();
  }
}
```

## 使う側のソースコード
FloatingActionButtonを押すと、ぴょんぴょんするようにします

```dart:main.dart
class MyHomePage extends StatelessWidget {
  final bounceIcon = BounceContainer(
      child: Icon(Icons.tag_faces)
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 0,
      ),
      body: Center(
        child: bounceIcon,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          bounceIcon.bounce();
        },
        child: Icon(Icons.repeat),
      ),
    );
  }
}
```

## アニメーションの種類で対空時間を調整する
TweenSequenceのCurvesによって、ぴょんぴょん具合を調整することができます
[Curvesアニメーションパターンは公式にあります](https://api.flutter.dev/flutter/animation/Curves-class.html)

* easeIn/Out(左)
* easeIn/OutCubic(中央)
* easeIn/OutCirc(右)

![ease.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/47b2e3b6-fa21-7b49-d833-9c7e4ea03546.gif)  ![ease_cubic.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/33a9b310-0c6b-9d19-e51d-08d4bc15c33f.gif)  ![ease_circ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/a208779a-2e5d-bb91-d954-51086e6ececc.gif)