【flutter】TweenSequenceとuseAnimationControllerを用いて複雑なアニメーションを実装する

## はじめに
Flutterでは、アニメーションを実装するためにTweenやAnimationControllerなどの便利なクラスが提供されています。今回は、TweenSequenceとuseAnimationControllerを組み合わせて、通知アニメーションを実装してみました。
![notification.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984024/01be1485-dfd2-3347-dda4-87bcad48063d.gif)


## 開発環境
- Flutter: 3.14.0
- Dart: 3.2.0

## パッケージのインストール
[flutter_hooks](https://pub.dev/packages/flutter_hooks)と[hooks_riverpod](https://pub.dev/packages/hooks_riverpod)というパッケージを利用します。
```yaml
  flutter_hooks: ^0.20.5
  hooks_riverpod: ^2.5.1
```

## useAnimationControllerとは
[useAnimationController](https://pub.dev/documentation/flutter_hooks/0.21.0/flutter_hooks/useAnimationController.html)は、FlutterHooksパッケージに含まれるフックの一つです。これを使用することで、アニメーションの開始、停止、逆再生など簡単に操作することができます。
## useAnimationControllerの使い方
useAnimationControllerの使い方を簡単に説明します。

以下のようにしてuseAnimationControllerを定義することができます。`duration`にはアニメーション全体の所要時間を設定します。
```
final animationController = useAnimationController(duration: const Duration(seconds: 2));
```
今回は以下２つのメソッドを使用しました。
|メソッド|用途|
|----|----|
|`animationController.forward();`|アニメーションの開始|
|`animationController.reset()`|アニメーションのリセット|

## TweenSequenceとは
`TweenSequence`は、Tweenのリストを使用して、時間にそってアニメーションの値(tween.value)を変化させることができるクラスです。今回実装する通知アニメーションのように、複数の段階で変化するアニメーションを実装する場合に活躍します。

## TweenSequenceの使い方
[TweenSequence](https://api.flutter.dev/flutter/animation/TweenSequence-class.html)は[TweenSequenceItem](https://api.flutter.dev/flutter/animation/TweenSequenceItem-class.html)のリストを持ちます。各TweenSequenceItemには`tween`と`weight`を指定します。
- tween （begine: 始点, end: 終点）
- weight （そのTweenがアニメーションの進行に占める時間の割合）

例えば、以下のコードではアニメーション全体の時間が5秒なので
5秒の40% = 2秒, 20% = 1秒より以下の表のように変化します。
|経過時間|tween.value（一次関数的に変化）|
|--|--|
|0~2秒|0→100に変化|
|2~3秒|100で一定|
|3~5秒|100→0に変化|

```dart
final animationController =
        useAnimationController(duration: const Duration(seconds: 5));
    final tween = TweenSequence([
      TweenSequenceItem(
        tween: Tween(
          begin: 0,
          end: 100,
        ),
        weight: 40,
      ),
      TweenSequenceItem(
        tween: ConstantTween(100),
        weight: 20,
      ),
      TweenSequenceItem(
        tween: Tween(
          begin: 100,
          end: 0,
        ),
        weight: 40,
      ),
    ]).animate(animationController);
```

## Widgetの実装
簡単な通知のWidgetを作成しました。
```dart
Widget buildNotificationWidget(context) {
    return SafeArea(
      child: Material(
        type: MaterialType.transparency,
        child: Container(
          height: 100,
          width: MediaQuery.of(context).size.width,
          color: Colors.green,
          padding: const EdgeInsets.only(top: 50),
          child: const Center(
            child: Text('お知らせ'),
          ),
        ),
      ),
    );
  }
}
```

`AnimationBuilder`を使用して、アニメーションが更新されるたびにウィジェットそ再構築し、アニメーションの値に応じてウィジェットを配置します。
```dart
return AnimatedBuilder(
  animation: animationController,
  builder: (context, _) {
    return Stack(
      children: [
        Positioned(
          top: tween.value,
          child: buildNotificationWidget(context),
        ),
        // 他のウィジェットの配置
      ],
    );
  },
);
```

## コード全文
これらを組み合わせることで通知アニメーションのような複雑なアニメーションを構築することが可能になります。以下のコードではボタンが押された時に通知アニメーションが開始するようになっています。

```dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class NotificationWidget extends HookConsumerWidget {
  const NotificationWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ステータスバーの高さ + ウィジェットの高さ + 上マージン + 影を考慮
    final movedPosition = -MediaQuery.of(context).padding.top;
    final initialPosition = -(MediaQuery.of(context).padding.top + 52 + 8);
    final animationController =
        useAnimationController(duration: const Duration(seconds: 4));
    final tween = TweenSequence([
      TweenSequenceItem(
        tween: Tween(
          begin: initialPosition,
          end: movedPosition,
        ),
        weight: 0.5,
      ),
      TweenSequenceItem(
        tween: ConstantTween(movedPosition),
        weight: 5,
      ),
      TweenSequenceItem(
        tween: Tween(
          begin: movedPosition,
          end: initialPosition,
        ),
        weight: 0.5,
      ),
    ]).animate(animationController);

    animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        animationController.reset();
      }
    });

    return AnimatedBuilder(
      animation: animationController,
      builder: (context, _) {
        return Positioned(
          top: tween.value,
          child: buildNotificationWidget(context),
        );
      },
    );
  }

  Widget buildNotificationWidget(context) {
    return SafeArea(
      child: Material(
        type: MaterialType.transparency,
        child: Container(
          height: 100,
          width: MediaQuery.of(context).size.width,
          color: Colors.green, // 後で直す
        ),
      ),
    );
  }
}

```

## 最後に
TweenSequenceとuseAnimationControllerを使えばどんな複雑なアニメーションでも作れる気がしてきてますね。ワクワクしました。