Flutter自作アニメーションの作り方


# Flutter自作アニメーションの作り方

## この記事でやること

- 0% から 100% に文字を変化させるアニメーションを作成する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472945/0b8eceae-e7d2-dc4c-7fc9-b35bd341b61a.gif" width=30%>

HeroWidgetなど公式のWidgetを使うとFlutterでは簡単に単純なアニメーションを作成することができるが、
自分で細かく調整したい場合はAnimationControllerを使った方がやりやすかったので紹介。

面倒くさいのでHeroWidgetのようなアニメーションを付けてくれるWidgetをまず先に探して、
どうしても無かった場合はこの方法でどうにかする、というスタンスでいいと思う。


## 事前知識

- Flutter Stateful Widget の使い方
    - setStateでWidgetを作り直すなど


## 全体の流れ

- テスト画面作成
- Ticker作成
- Controller作成
- 文字列に代入

## テスト画面を作る

文字を中央に表示するだけ。

今回はmain.dartを書き換えているが、適宜アニメーションを付けたいWidgetに置き換える。
statefull widgetで作る必要があることに注意。

以下サンプルコード

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

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text('custom animation test'),
        ),
        body: Center(
          child: Text(
            //TODO animate text from 0 to 100%
            '0%',
            style: TextStyle(
              fontSize: 40.0,
            ),
          ),
        ),
      ),
    );
  }
}

```

<img width="489" alt="Screen Shot 2020-03-25 at 1.01.53 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472945/810f495d-5daf-d444-22e9-a56ee32a1d2d.png">


## アニメーション作成

### Ticker作成

まずはTickerを作る。

Tickerは0から1の値を変動していく。
Tickerの値が変化するたびに新しいフレームを作成していき、繋げてみると動いているように見えるということ。

これは単純にアニメーションを付けたいWidgetにMixinをつけることで作成できる。

例えば作るアニメーションが１つの場合は

`with SingleTickerProviderStateMixin`

をアニメーションをしたいWidgetに付ける。

サンプル

```dart

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    ....
```

### Controller作成

アニメーションの開始するタイミングや、変動する範囲を決められる。

今回は0から100までの値を3秒間かけて変化するコントローラーを作る。
コントローラーはWidget作成時に最初に完成している必要があるのでinitState()内で作成する。

(flutterのwidgetが実行される順番は initState >>> build >>> dispose)

サンプル

```dart
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  AnimationController controller;

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

    controller = AnimationController(
      vsync: this, // このWidgetのTickerと同期するように指定
      duration: Duration(seconds: 3),
      lowerBound: 0.0, 
      upperBound: 100.0, 
    );
  }

  @override
  Widget build(BuildContext context) {
    ...

```

アニメーションが終わったらコントローラーを削除する。
これを行わないとWidgetが削除されてもコントローラーの計算が永遠と行われることになる。

Widget削除時に実行されるdispose内に記述する。

```dart

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

```

## Controllerを動かす

値が0から100まで３秒間かけて変化するControllerを

initState内で開始させるコマンド`controller.forward();`を書き、

Widget作成時に実行されているか確認してみる。

サンプル

```dart

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

    controller = AnimationController(
      vsync: this, // このWidgetのTickerと同期するように指定
      duration: Duration(seconds: 3),
      lowerBound: 0.0,
      upperBound: 100.0,
    );

    controller.forward(); // アニメーション開始

    controller.addListener(() {
      print(controller.value); // コンソールで値を確認
    });
  }

```



実行結果

Hot Restartすると

```

Restarted application in 956ms.
flutter: 0.0
flutter: 0.0
flutter: 12.396533333333332
flutter: 12.9521
flutter: 14.0632
flutter: 14.618733333333333
flutter: 15.174299999999999
flutter: 15.174299999999999
flutter: 15.174299999999999

...

flutter: 94.598
flutter: 95.15356666666666
flutter: 95.70913333333334
flutter: 96.26466666666667
flutter: 96.82023333333333
flutter: 97.3758
flutter: 97.93133333333334
flutter: 98.4869
flutter: 99.0425
flutter: 99.598
flutter: 100.0
```

のように値が0から100まで変化していることが確認できる。

## Contoroller.value をテキストに代入

`Contoroller.value`が変化するたびにWidgetを作り直したい。

そのために`controller.addListener`内に`setState`を書く。

```dart

    controller.addListener(() {
      print(controller.value);
      setState(() {});
    });
```

main.dart内で0%を表示していたテキストを`controller.value.toInt()`に置き換える。

```dart
          child: Text(
            //TODO animate text from 0 tp 100%
            '${controller.value.toInt()}%',
            style: TextStyle(
              fontSize: 40.0,
            ),
          ),
```

Hot Restartすると

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472945/0b8eceae-e7d2-dc4c-7fc9-b35bd341b61a.gif" width=30%>

めんどう