Flutter アニメーション (Flutter Roadmap Animations)

## はじめに
Flutterを網羅的に学習するにあたって[Roadmap](https://roadmap.sh/flutter)を使って学習を進めることにしました。

この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、**アニメーション**についてまとめています。

RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひ[Roadmap](https://roadmap.sh/)を利用して学習してみてください。

## Roadmapとは

簡潔に言えば、Roadmap.shは学習者にとっての**ガイドブック**であり、**学習の方向性を提供する学習ロードマップサイト**です。

初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。

学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。

## Animations
FlutterロードマップAnimationsでは以下の15のサイトが紹介されています。興味のある方はぜひお読みください。

- Introduction to animations: https://docs.flutter.dev/ui/animations
- Animation library: https://api.flutter.dev/flutter/animation/animation-library.html
- Hero animations: https://docs.flutter.dev/ui/animations/hero-animations
- HeroAnimation class: https://docs.flutter.dev/ui/animations/hero-animations#heroanimation-class
- Hero class: https://api.flutter.dev/flutter/widgets/Hero-class.html
- Fade a widget in and out: https://docs.flutter.dev/cookbook/animation/opacity-animation
- AnimatedOpacity widget: https://docs.flutter.dev/codelabs/implicit-animations#animate-opacity-with-animatedopacity-widgets
- Curved­Animation: https://docs.flutter.dev/ui/animations/tutorial
- CurvedAnimation Class: https://api.flutter.dev/flutter/animation/CurvedAnimation-class.html
- AnimationController - Flutter: https://docs.flutter.dev/ui/animations/tutorial#animationcontroller
- AnimationController class: https://api.flutter.dev/flutter/animation/AnimationController-class.html
- AnimatedBuilder Class: https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html
- Refactoring with AnimatedBuilders: https://docs.flutter.dev/ui/animations/tutorial#refactoring-with-animatedbuilder
- Simplifying with Animated­Widget: https://docs.flutter.dev/ui/animations/tutorial#simplifying-with-animatedwidgets
- AnimatedWidget Class: https://api.flutter.dev/flutter/widgets/AnimatedWidget-class.html


## アニメーションとは
Flutterにおけるアニメーションは、アプリケーションのユーザーインターフェース(UI)を際立たせるための重要な機能です。アプリの開発でUIは当然作らなければなりませんが、アニメーションに関しては無くても要件を満たせることが多いため、結局導入しないこと事も多いかと思います。しかし効果的なアニメーションを入れる事でアプリの完成度が一気に上がり、同じ機能でもユーザーに与える印象は全く異なってくるので、この機会にぜひ学習してみてください。

## アニメーションの基本
1. **アニメーションの対象:** アニメーションを適用する対象（ウィジェット、要素、プロパティなど）を選択します。Flutterではほとんどのウィジェットとプロパティにアニメーションを適用できます。

2. **アニメーションコントローラ:** アニメーションを制御するために、 `AnimationController` を使用します。これにより、アニメーションの開始、停止、逆再生などが制御できます。

3. **アニメーションの定義:** アニメーションの動作を定義します。たとえば、開始値から終了値、アニメーションの速さ、アニメーションの期間などを指定します。

4. **アニメーションウィジェット:** アニメーションをウィジェットに適用します。Flutterでは、 `AnimatedContainer` 、 `AnimatedOpacity` 、 `SlideTransition` など、さまざまなアニメーション用のウィジェットが提供されています。

5. **アニメーションの実行:** アニメーションを実行するために `AnimationController` を使用し、 `Animation` オブジェクトを使用してアニメーションの進行状況にアクセスします。

### アニメーション制御
アニメーションを制御し、ウィジェットに反映するためには、主に以下の3つのクラスを使用します。

#### AnimationController:
`AnimationController` はアニメーションを制御するためのコントローラーです。アニメーションの開始、停止、逆再生、速度調整などの操作を管理します。

```dart: AnimationControllサンプルコード
final AnimationController controller = AnimationController(
  duration: const Duration(seconds: 2),
  vsync: this, 
);
```

#### AnimatedBuilder:

`AnimatedBuilder` は `AnimationController` に接続し、アニメーションの進行に合わせてウィジェットを再構築するウィジェットです。アニメーションの値が変化するたびに、新しいウィジェットを作成します。

```dart: AnimatedBuilderサンプルコード
AnimatedBuilder(
  animation: controller, 
  builder: (BuildContext context, Widget? child) {
    return Container(
      height: controller.value * 100, 
      width: controller.value * 100,
      color: Colors.blue,
    );
  },
)
```

#### AnimatedWidget:

`AnimatedWidget`は、Flutterでアニメーションを扱うためのWidgetで、アニメーションオブジェクトを監視して、アニメーションの値が変化した際に、自動的に再描画します。

```dart: AnimatedWidgetサンプルコード
import 'package:flutter/material.dart';


void main() => runApp(const AnimatedWidgetExampleApp());

class AnimatedWidgetExampleApp extends StatelessWidget {
  const AnimatedWidgetExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: AnimatedWidgetExample(),
    );
  }
}

class SpinningContainer extends AnimatedWidget {
  const SpinningContainer({
    super.key,
    required AnimationController controller,
  }) : super(listenable: controller);

  Animation<double> get _progress => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    return Transform.scale(
      scale: _progress.value, 
      child: Container(width: 200.0, height: 200.0, color: Colors.green),
    );
  }
}

class AnimatedWidgetExample extends StatefulWidget {
  const AnimatedWidgetExample({super.key});

  @override
  State<AnimatedWidgetExample> createState() => _AnimatedWidgetExampleState();
}

class _AnimatedWidgetExampleState extends State<AnimatedWidgetExample>
    with TickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 10),
    vsync: this,
  )..repeat();

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

  @override
  Widget build(BuildContext context) {
    return SpinningContainer(controller: _controller);
  }
}
```
**1. `SpinningContainer` クラス:**
- `AnimatedWidget` を継承しています。これにより、アニメーションの進捗に応じて `build` メソッドで自動的に再描画します。

- コンストラクタで `AnimationController` を必須のパラメーターとして受け取り、親クラスのコンストラクタにそのコントローラーを渡しています。

- `Animation<double>` 型の `_progress` を作成しています。 `listenable` プロパティは、親クラスの `AnimatedWidget` に定義されています。これは `AnimationController` を監視し、その変更を反映させるプロパティです。
`as Animation<double>` で `listenable` を `Animation<double>` 型にキャストしています。これによって、 `Animation<double>` のインスタンスとして使用できます。

- `build` メソッドでは、`Transform.scale` を使用して `child` の `Container` のサイズを変更します。スケーリングの値は `_progress.value` によって制御されています。

**2. `_AnimatedWidgetExampleState`クラス:**

- `with TickerProviderStateMixin` は、 アニメーションを制御するために使用される TickerProvider の一つです。
- `_controller` は `AnimationController` 型で、アニメーションの制御をします。
`duration` でアニメーションの継続時間を設定しており、 `vsync` は画面表示と動作の同期を取るためのプロパティです、ここでは `TickerProviderStateMixin` を使用しています。 また`..repeat()` でアニメーションを繰り返すように指定しています。

- `dispose` メソッドは、 `_AnimatedWidgetExampleState` オブジェクトが不要になったときにリソースを削除するために呼び出されます。ここでは、 `_controller` を削除しています。

- `build` メソッドでUIを構築しています。ここでは `SpinningContainer` ウィジェットを返しており、このウィジェットを作成した `_controller` を使用してアニメーションを制御しています。

## Heroアニメーション

Heroアニメーションは、タップした画像が大きくなったり移動したりしながら画面遷移の動きをするアニメーションです。遷移先の画面に遷移元と似ている要素がある場合にHeroアニメーションを使用することでシームレスに画面遷移することができます。

### サンプルコード
```dart: Heroサンプルコード
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FirstPage(),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Page'),
      ),
      body: Center(
        child: GestureDetector(
          child: Hero(
            tag: 'textTag',
            child: Text(
              'Hello, Flutter!',
              style: TextStyle(fontSize: 24.0),
            ),
          ),
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute(
              builder: (context) => SecondPage(),
            ));
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Center(
        child: GestureDetector(
          child: Hero(
            tag: 'textTag',
            child: Text(
              'Hello, Flutter!',
              style: TextStyle(fontSize: 36.0),
            ),
          ),
          onTap: () {
            Navigator.of(context).pop();
          },
        ),
      ),
    );
  }
}
```

**1. `FirstPage` クラス:**
- `Scaffold` ウィジェットを使用して `AppBar` と `body` を含めています。

- `body` には `Hero` ウィジェットが含まれており、 `tag` プロパティでタグ "textTag" を指定しています。この`Hero`ウィジェットがアニメーションの対象となります。

- `GestureDetector` ウィジェットは `Hero` ウィジェットをラップし、ユーザーのタップ操作を検出します。タップ操作が検出されると、 `Navigator` を使用して `SecondPage` に遷移します。

**2. `SecondPage` クラス:**
- 最初のページから遷移してきたテキストをより大きなフォントサイズで表示しています。
 
- `body` には `Hero` ウィジェットが含まれており、最初のページと同じタグ "textTag" を使用しています。このページでも `GestureDetector` ウィジェットが使用され、タップ操作が検出されると前のページに戻ります。

Heroアニメーションを使用する上で、重要なことは、アニメーションをしたいオブジェクトに対して、**同じ名前のtagを設定する**ことです。Flutterではtagを使って、対象のwidgetが画面のどこにあるのかを管理しています。`Hero`でラップするwidgetツリーの構造を画面間で揃えておくことで、アニメーションが行えるようになります。

## Opacityアニメーション
Opacityアニメーションは、ウィジェットの不透明度（透明度）を変更します。ウィジェットの表示・非表示アニメーションによく使用されます。

### サンプルコード
```dart: Opacityサンプルコード
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyPage(),
    );
  }
}

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  bool _isVisible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Opacity Animation Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedOpacity(
              opacity: _isVisible ? 1.0 : 0.0,
              duration: Duration(seconds: 1),
              child: Text(
                'Hello, Flutter!',
                style: TextStyle(fontSize: 24.0),
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _isVisible = !_isVisible; 
                });
              },
              child: Text('Toggle Opacity'),
            ),
          ],
        ),
      ),
    );
  }
}
```

**1. `MyPage`クラス:**
- `StatefulWidget` を継承しており、アプリの画面を構築しています。このクラスでは `_isVisible` という状態を管理しています。これは、テキストの表示/非表示を制御しています。

- `body` 部分では、 `AnimatedOpacity` ウィジェットが使用されています。このウィジェットは"Hello, Flutter!" テキストの透明度を `_isVisible` の値に応じてアニメーション化します。 `duration` プロパティでアニメーションの時間を設定し、 `child` プロパティで表示するテキストを指定しています。

- `ElevatedButton` ウィジェットでボタンを表示しています。ボタンがタップされると、 `onPressed` コールバック内で `_isVisible` の値を反転させ、テキストの表示/非表示を切り替えます。

## CurvedAnimation
CurvedAnimation は、アニメーションの進行速度をカスタマイズするアニメーションの一種です。このクラスを使用することで、アニメーションの開始から終了までの速度を一定ではなく、特定の速度に変化させることができます。

### サンプルコード
```dart: CurvedAnimationサンプルコード
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  AnimationController? _controller;
  Animation<double>? _animation;

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

    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );

    final CurvedAnimation curve = CurvedAnimation(
      parent: _controller!,
      curve: Curves.bounceOut, 
    );

    _animation = Tween<double>(
      begin: 0,
      end: 1,
    ).animate(curve);

    // アニメーションの進行状況を監視
    _animation!.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller!.reverse(); // アニメーションが終了したら逆再生
      } else if (status == AnimationStatus.dismissed) {
        _controller!.forward(); // アニメーションが最初から再生
      }
    });

    // アニメーションの再生
    _controller!.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CurvedAnimation Example'),
      ),
      body: Center(
        child: ScaleTransition(
          scale: _animation!,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller!.dispose(); 
    super.dispose();
  }
}
```
**1. `MyHomePage` クラス:**

- `StatefulWidget` を継承し、 `_MyHomePageState` クラスを生成します。
アニメーションがウィジェットのライフサイクルに同期するために `SingleTickerProviderStateMixin` を使用してアニメーションコントローラーに`vsync`を設定しています。

:::note info

**SingleTickerProviderStateMixinとは**
アニメーションを制御するために使用される TickerProvider の一つです。SingleTickerProviderStateMixin は単一のアニメーションコントローラー（AnimationController）を使用する際に、利用されます。

Flutterのアニメーションは、タイミングに合わせてアニメーションを行うためにタイミング情報（ティッカー情報）が必要です。ティッカー情報はTickerProvider を実装したクラスから提供されています。

SingleTickerProviderStateMixin は、単一のアニメーションコントローラーのタイミング情報を提供しています。アプリ内で複数のアニメーションがある場合、SingleTickerProviderStateMixinを使用することで、アニメーションを同期させることができます。一方、複数のアニメーションコントローラーが必要な場合は TickerProviderStateMixin を使用します。

SingleTickerProviderStateMixin を使うことで、アニメーションコントローラーがウィジェットのライフサイクルと同期され、正確なタイミングでアニメーションを制御できるようになります。
:::


**2. `_MyHomePageState` クラス:**

- `AnimationController` オブジェクト `_controller` を初期化し、アニメーションのパラメータを設定しています。ここでは、アニメーションの時間は2秒に設定し、`vsync`には`_MyHomePageState`を使用してウィジェットのライフサイクルと同期させています。

- `CurvedAnimation` オブジェクト `curve` を作成し、 `Curves.bounceOut`を指定しています。これはアニメーションの速度を指定するために使用されます。

- `Tween` を使用してアニメーションの範囲を 0 から 1 まで設定し、 `animate` メソッドを呼び出してアニメーションを作成しています。

- `addStatusListener` メソッドを使用して、アニメーションの進行状況を監視し、アニメーションが完了したら逆再生するように設定しています。

- `_controller.forward()` メソッドを呼び出してアニメーションを開始しています。

**3. `build`メソッド:**

- アプリのUIを構築しています。`Scaffold`ウィジェットを使用し、`AppBar`とアニメーション化された`Container`を表示しています。

- `ScaleTransition` ウィジェットを使用して、 `_animation` をアニメーションのスケールに適用し、コンテナを拡大/縮小しています。

**4. `dispose`メソッド:**

- ウィジェットが破棄されたときに、アニメーションコントローラーのリソースを削除します。

## 参考資料

https://www.flutter-study.dev/create-ui/animation

https://zenn.dev/flutteruniv_dev/articles/a9ee3695cfcf63

https://engineering.nifty.co.jp/blog/13929

https://bukiyo-papa.com/hero/

https://bukiyo-papa.com/opacity/

https://flutter.salon/widget/animatedwidget/

https://api.flutter.dev/flutter/widgets/AnimatedWidget-class.html