Flutterのアニメーション(animated/transition)を色々動かして整理してみる

---
# :book: Flutterの記事を整理し本にしました :book:
https://zenn.dev/kazutxt/books/flutter_practice_introduction

* <font color="red"><b>本稿の記事を含む様々な記事を体系的に整理し本にまとめました</b></font>
* 今後はこちらを最新化するため、<font color="red"><b>最新情報はこちらをご確認ください</b></font>
* 20万文字を超える超大作になっています!!

---

## まとめ記事
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)

# はじめに
* いままであまりアニメーションに手を出してこなかったのですが、UI/UXを考えると重要なので、GWをの時間を使って、色々動かしながら整理してみました


# 概要
Flutterにはアニメーションを簡単に取り扱えるWidgetが潤沢に準備されています。
Flutterでアニメーションを扱えるWidgetは大きく2種類あります。

* パラメタを設定するだけで簡単に取り扱えるanimated系
* ontrollerとセットで利用し、細かい制御ができるtransition系

この2種類を解説していきます。

# Animated系Widget
簡単にアニメーションがあつかえるanimated系は、`AnimatedXXX`という名称のWidgetが該当します

## AnimatedOpacity,AnimatedSize,AnimatedAlign

まずは、Opacity(不透明度)、Size(大きさ)、Align(位置)をアニメーションで動かしてみます。

いずれも、`AnimatedXXX`というWidgetに対して、`duration`でどの程度の期間をかけて変化をするかというパラメタと、対象となるWidgetをchildで指定します。

```dart:main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);
  final String? title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  bool flag = false;

  _click() async {
    setState(() {
      flag = !flag;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedOpacity(
                opacity: flag ? 0.1 : 1.0,
                duration: Duration(seconds: 3),
                child: Text(
                  "消える文字",
                  style: Theme.of(context).textTheme.headline4,
                )),
            AnimatedSize(
                vsync: this,
                duration: Duration(seconds: 3),
                child: SizedBox(
                    width: flag ? 50 : 200,
                    height: flag ? 50 : 200,
                    child: Container(color: Colors.purple))),
            AnimatedAlign(
                duration: Duration(seconds: 3),
                alignment: flag ? Alignment.topLeft : Alignment.bottomRight,
                child: SizedBox(
                    width: 50,
                    height: 50,
                    child: Container(color: Colors.green)))
          ],
        ),
      ),
      floatingActionButton:
          Row(mainAxisAlignment: MainAxisAlignment.end, children: [
        FloatingActionButton(onPressed: _click, child: Icon(Icons.add)),
      ]),
    );
  }
}
```

まず、`AnimatedOpacity`,`AnimatedSize`,`AnimatedAlign`の3つが`Column`で設定されています。
次に、ボタンをクリックするたびにフラグが切り替わり、アニメーションの2つの状態を切り替えています。

次に、3つのWidgetの詳細は下記のようになっています。

* Opacity:"消える文字"のテキストの透明度を0.1と1.0を切り替えています
* Size:50×50と200×200のサイズを切り替えています
* Align:配置の左上と右下を切り替えています。

Animation用Widgetのパラメタではなく、childで指定するWidgetの状態を切り替えているものもあります。

![pic21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/39ebebe9-8308-7765-86b6-5fc451440754.png)


## AnimatedContainer,AnimatedSwitcher

前述の`AnimatedSize`や`AnimatedAlign`は基本的で汎用性は高いのですが、もう少しまとめて使いたい場合もあります。このような場合には`AnimatedContainer`や`AnimatedSwitcher`が便利です。

`AnimatedContainer`は要素をまとめて変更する場合に便利です。`AnimatedSwitcher`は2つのWidgetをいい感じに切り替えてくれる便利なアニメーションWidgetです。

```dart:main.dart
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }

class _MyHomePageState extends State<MyHomePage> {
  bool flag = false;
  _click() async {
    setState(() {
      flag = !flag;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedContainer(
                duration: Duration(seconds: 3),
                width: flag ? 100 : 50,
                height: flag ? 50 : 100,
                padding: flag ? EdgeInsets.all(0) : EdgeInsets.all(30),
                margin: flag ? EdgeInsets.all(0) : EdgeInsets.all(30),
                transform: flag ? Matrix4.skewX(0.0) : Matrix4.skewX(0.3),
                color: flag ? Colors.blue : Colors.grey),
            AnimatedSwitcher(
                duration: Duration(seconds: 3),
                child: flag
                    ? Text("なにもない")
                    : Icon(Icons.favorite, color: Colors.pink))
          ],
        ),
      ),
      floatingActionButton:
          Row(mainAxisAlignment: MainAxisAlignment.end, children: [
        FloatingActionButton(onPressed: _click, child: Icon(Icons.add)),
      ]),
    );
  }
}
```
今度は、`AnimatedContainer`と`AnimatedSwitcher`の2つがColumnで設定されています。
ボタンでフラグのON/OFFを切り替えている部分は変わっていません。

* Container:width,height,padding,marginなどの各要素の幅を変更しながら、transformで形を変え、colorで色も変えています。

* Switcher:TextのWidgetとIconのWidgetを3秒間で変えています。全く異なるWidgetですが、なめらかにアニメーションでつなぐことができます。

![](https://storage.googleapis.com/zenn-user-upload/qpcyp96lde6yl75ilzztwav4s1xy)

# Transition系Widget

細かい制御ができるtransition系は、`XXXTransition`という名称のWidgetが該当します


## AnimationContoroller
Transition系は、`AnimationController`を使って、アニメーションの再生・停止・繰り返し・逆再生などを行います。

基本的な流れは、`AnimationController`を作成し、Transition系Widgetにパラメタとして設定します。


```dart:main.dart
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late AnimationController _animationControler;
  _play() async {
    setState(() {
      _animationControler.forward();
    });
  }

  _stop() async {
    setState(() {
      _animationControler.stop();
    });
  }

  _reverse() async {
    setState(() {
      _animationControler.reverse();
    });
  }

  @override
  void initState() {
    super.initState();
    _animationControler =
        AnimationController(vsync: this, duration: Duration(seconds: 3));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizeTransition(
              sizeFactor: _animationControler,
              child: Center(
                  child: SizedBox(
                      width: 50,
                      height: 50,
                      child: Container(color: Colors.green))),
            ),
          ],
        ),
      ),
      floatingActionButton:
          Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        FloatingActionButton(
            onPressed: _play, child: Icon(Icons.arrow_forward)),
        FloatingActionButton(onPressed: _stop, child: Icon(Icons.pause)),
        FloatingActionButton(
            onPressed: _reverse, child: Icon(Icons.arrow_back)),
      ]),
    );
  }
}
```

`AnimationController`は、`initState`で生成し、`dispose`で破棄します。

`dipose`で破棄しない場合は、メモリリークになる可能性があるので注意してください。

再生ボタンで`_forward`、停止ボタンで`_stop`、逆再生ボタンで`_reverse`を呼び出し、対応する`AnimationController`のメソッドを呼び出しています。

![pic41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/3003cbba-3f8a-41c2-5d82-507505710957.png)


## Animation/Tween

`AnimationController`は、`Duration`の間に、`0.0`から`1.0`まで`value`を変化させながら、アニメーションを実現しています。

`Tween`を使うと、この範囲や型を自由にカスタマイズした`Animation`を作成することができます。

なお、`AnimationController`と`Tween`を連携させ、`Animation`を作成する方法は、以下の2種類があります。
1. TweenのanimateメソッドにAnimationControllerを渡す
2. AnimationControllerのdriveメソッドにTweenを渡す

今回は1の方法を採用しています。

```dart:main.dart
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }


class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late AnimationController _animationControler;
  late Animation<double> _animationDouble;
  Tween<double> _tweenDouble = Tween(begin: 0.0, end: 200.0);
  late Animation<Color?> _animationColor;
  ColorTween _tweenColor = ColorTween(begin: Colors.green, end: Colors.blue);

  _play() async {
    setState(() {
      _animationControler.forward();
    });
  }

  _stop() async {
    setState(() {
      _animationControler.stop();
    });
  }

  _reverse() async {
    setState(() {
      _animationControler.reverse();
    });
  }

  @override
  void initState() {
    super.initState();
    _animationControler =
        AnimationController(vsync: this, duration: Duration(seconds: 3));

    _animationDouble = _tweenDouble.animate(_animationControler);
    _animationDouble.addListener(() {
      setState(() {});
    });
    _animationColor = _tweenColor.animate(_animationControler);
    _animationColor.addListener(() {
      setState(() {});
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("AnimationController:${_animationControler.value}"),
            Text("AnimationDouble:${_animationDouble.value}"),
            Text("AnimationColor:${_animationColor.value}"),
            SizeTransition(
              sizeFactor: _animationControler,
              child: Center(
                  child: SizedBox(
                      width: _animationDouble.value,
                      height: _animationDouble.value,
                      child: Container(color: _animationColor.value))),
            ),
          ],
        ),
      ),
      floatingActionButton:
          Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        FloatingActionButton(
            onPressed: _play, child: Icon(Icons.arrow_forward)),
        FloatingActionButton(onPressed: _stop, child: Icon(Icons.pause)),
        FloatingActionButton(
            onPressed: _reverse, child: Icon(Icons.arrow_back)),
      ]),
    );
  }
}
```
`Tween`に型を指定して宣言し、`begin`と`end`で開始と終了の状態の値を設定します。
その後、`_animationController`と紐付けたアニメーションを作成しています。

アニメーションは、サイズ用とカラー用の2つを作成しています。
`_animationDouble`と`_animationColor`がそれに対応します。`Tween`もそれぞれに対応するものを作成しています。

`addListener(() {setState(() {});});`は変更を反映させるために行っています。

![pic42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/f5af20db-49ba-e308-dd0d-b34f883d9029.png)


今回は、サイズ管理用のDoubleを0~200に変化させながら、カラー管理用をグリーンからブルーまで変化させています。


## AnimationBuilder

より汎用的に使う方法として、`AnimationBuilder`があります。
`animation`パラメタにどのようなアニメーションを与え、`builder`にアニメーションで動かすWidgetを構築します。

```dart:main.dart
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late AnimationController _animationControler;
  late Animation _animation;

  _play() async {
    setState(() {
      _animationControler.forward();
    });
  }

  _stop() async {
    setState(() {
      _animationControler.stop();
    });
  }

  _reverse() async {
    setState(() {
      _animationControler.reverse();
    });
  }

  @override
  void initState() {
    super.initState();
    _animationControler =
        AnimationController(vsync: this, duration: Duration(seconds: 1));
    _animation = _animationControler.drive(Tween(begin: 0.0, end: 2.0 * pi));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _animation,
          builder: (context, _) {
            return Transform.rotate(
                angle: _animation.value, child: Icon(Icons.cached, size: 100));
          },
        ),
      ),
      floatingActionButton:
          Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        FloatingActionButton(
            onPressed: _play, child: Icon(Icons.arrow_forward)),
        FloatingActionButton(onPressed: _stop, child: Icon(Icons.pause)),
        FloatingActionButton(
            onPressed: _reverse, child: Icon(Icons.arrow_back)),
      ]),
    );
  }
}

```

この例では、`AnimationController`の`drive`メソッドに`Tween`を渡す方法で`Animation`を作成しています。

まず、`initState`でvalueを0~2πまでを変化させるアニメーションを作成しています。

続いて、AnimatedBuilderを作り、その中でアニメーションを設定し、builderでそのアニメーションを適応するWidgetを指定します。
今回は回転処理をを行うWidgetを使っています。

![pic43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/e35ce20e-b0f0-1cdf-f4b3-bd5d99ee4f0d.png)


# TickerProviderStateMixinについて

mixinを使っていますが、これは`vsync:this`を使うためで、vsyncはアニメーションのフレームごとの更新を伝播するために用いられます。

単一のAnimationControllerで使う場合は`SingleTickerProviderStateMixin`を使い、複数で使う場合は`TickerProviderStateMixin`を使います。

基本的に自作することはなく、おまじないのように利用して問題ないかと思います。