Flutter アニメーション入門

Flutter アニメーション入門

Flutter で アニメーション

Flutter は簡単に美麗なモバイルアプリを作成できるフレームワークです。アニメーションも簡単に実装できます。

ここでは、アニメーションの基本をまとめます。

Flutter はドキュメントがしっかりしているので、公式ページを読むとだいたいは理解できます。アニメーションについては、以下のページを読むと理解できます。

アニメーションの種類

Flutter には、2種類のアニメーションがあります。1つは Tween ベースのアニメーションで、もう一つは物理ベースのアニメーションです。

今回はよく使うであろう Tween ベースのアニメーションを見ていきます。

Tween とは

Tween ベースのアニメーションは、開始点から終了点までの状態を表現するアニメーションです。たとえば赤色から青色に変化するアニメーションや、座標0から100まで移動するアニメーションのことです。

開始終了の2点間をどのように変換するかを定義することで様々なアニメーションを再現できます。

アニメーションの基本パーツ

Flutter でアニメーションを実装するには、以下の4パーツの役割を理解する必要があります。

  1. Animation
  2. AnimationController
  3. Tween
  4. addListener

Animation

Animatin クラスはアニメーションの状態を保持します。この状態の変更に応じて画面が書き換わるようにすることでアニメーションを実装します。

AnimationController

AnimationController は、アニメーションを制御します。現在のアニメーションの再生状態を管理するのが役割です。アニメーションを再生したり、停止したり、逆再生したりできます。

与えられた条件に従い、Animation クラスの状態を書き換えます。

Tween

Tween は、アニメーションで制御する数値の間隔を定義します。たとえば 0~100 までといった数値や 赤色~青色 といった色の開始終了を定義できます。

この範囲内で AnimationControllerAnimation の状態を書き換えます。

addListener

Animation クラスは状態が書き換えられるたびに、すべてのリスナーに変更を通知します。

アニメーションを利用するWidgetは、変更が通知されるたびに setState() を呼び出すことで現在の状態を描画します。

簡単なアニメーションサンプル

ボタンを押すとサイズが変わる四角形をアニメーションを使って実装してみます。次のような感じの動きになります。

サンプルコード

以下、サンプルです。

import 'package:flutter/material.dart';

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

class AnimationRect extends StatefulWidget {
  @override
  _AnimationRectState createState() => _AnimationRectState();
}

class _AnimationRectState extends State<AnimationRect>
    with SingleTickerProviderStateMixin { // 1

  // 2
  Animation<double> animation;
  AnimationController controller;
  final tween = Tween(begin: 50.0, end: 300.0);

  // 3
  initState() {
    super.initState();

    // 2000ミリ秒のアニメーション
    controller = AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);

    // 50.0 から 300.0 の範囲でアニメーションを行う
    // 状態が書き換わると描画を更新する
    animation = tween.animate(controller)
      ..addListener(() {
        setState(() {});
      });
  }

  // 4
  startAnimation() {
    // アニメーションの状態を見て
    // 再生するか逆再生するかを決める
    switch (animation.status) {
      case AnimationStatus.dismissed:
      case AnimationStatus.reverse:
        controller.forward();
        break;
      case AnimationStatus.completed:
      case AnimationStatus.forward:
        controller.reverse();
        break;
    }
  }

  @override
  Widget build(BuildContext context) { // 5
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              RaisedButton(
                child: Text('Start Animation'),
                onPressed: () => startAnimation(),
              ),
              SizedBox(
                // アニメーションオブジェクトから現在の状態を取得して利用する
                width: animation.value,
                height: animation.value,
                child: Container(color: Colors.black),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

以下、簡単な解説です。

1. StatefulWidget を作成する

Animationを利用するWidgetはその状態を管理する必要があるので StatefulWidget である必要があります。

また、SingleTickerProviderStateMixin をクラス定義に追加します。これは、オフスクリーン時に不要なアニメーションでリソースを消費しないようにするための設定です。

よくわからないのですが、おまじないとして設定しておきます。

class _AnimationRectState extends State<AnimationRect>
    with SingleTickerProviderStateMixin { /*...*/ }

2. Animation と Controller

StatefulWidget の状態として、アニメーションとコントローラーを管理します。Tween クラスで管理する値の取りうる範囲を指定します。

今回は四角形の大きさを50~300の範囲でアニメーションさせるために次のような設定にしました。

// 2
Animation<double> animation;
AnimationController controller;
final tween = Tween<double>(begin: 50.0, end: 300.0);

そのほかにも色を変えたい場合は、Tween<Color> を使って開始終了の色を指定することも可能です。

3. アニメーションの定義

initState() は、Widgetのライフサイクルの1つで、生成直前に呼び出されます。

build() で、animation.value を参照する必要があるのでここで初期化しておきます。

controller は、アニメーションの時間を指定します。今回は2000ミリ秒(2秒)で完了するアニメーションを定義します。

Tween.animate() メソッドは、引数で指定されたコントローラーによってアニメーションを制御します。戻り値に Animation クラスのインスタンスが返されます。このインスタンスにリスナー処理を追加して、アニメーションの更新がかかるたびに、setState() を呼び出されるようにすることで画面の更新を行います。

// 3
initState() {
  super.initState();

  // 2000ミリ秒のアニメーション
  controller = AnimationController(
      duration: const Duration(milliseconds: 2000), vsync: this);

  // 50.0 から 300.0 の範囲でアニメーションを行う
  // 状態が書き換わると描画を更新する
  animation = tween.animate(controller)
    ..addListener(() {
      setState(() {});
    });
}

..adListener の記法について

..adListener の記法は初見ではかなり戸惑います。これはメソッドの カスケード呼び出し と呼ばれるもので、単一のオブジェクトに対して複数の操作を連続して行う場合などに便利な記法です。

以下の記法は同じ意味を持つコードです。

animation = tween.animate(controller)
  ..addListener(() {
    setState(() {});
  });

// 上のコードと同じ意味
animation = tween.animate(controller)
animation.addListener(() {
  setState(() {});
});

戻り値の animation に対してメソッドの呼び出しを列挙できるのが利点です。今回のように1つの処理を呼び出すだけだとあまりメリットはないかもですが、複数の処理を連続して呼び出す場合に、簡単に書けるようになっています。

4. アニメーションの開始

AnimationController.forword() でアニメーションを開始できます。Tweenで定義した範囲で状態が更新され、終了点まで値が変化していきます。終了点に達するとアニメーション終了です。

AnimationController.reverse() は逆方向にアニメーションを動かします。開始点に向かって状態が更新され、開始点まで達するとアニメーション終了です。

Animation クラスは現在のステータスを保持しています。これを参照することでアニメーションの再生状況を判定できます。ステータスの種類は以下の4つです。

  • AnimationStatus.dismissed => 初期状態
  • AnimationStatus.completed => 完了状態
  • AnimationStatus.forward => 再生状態
  • AnimationStatus.reverse => 逆再生状態

以下のコードはアニメーションのステータスを見て、再生/逆再生を呼び分けています。この関数をボタン押下時に呼び出すことで、アニメーションを開始します。

// 4
startAnimation() {
  switch (animation.status) {
    case AnimationStatus.dismissed:
    case AnimationStatus.reverse:
      controller.forward();
      break;
    case AnimationStatus.completed:
    case AnimationStatus.forward:
      controller.reverse();
      break;
  }
}

5. Widget でアニメーション描画

アニメーションクラスに現在の状態があります。今回利用しているのが Animation<double> で、50.0~300の範囲の数値を保持しています。これを四角形の幅と高さに設定しておくと、変更時に画面描画が更新され、アニメーションとして動くようになります。

@override
Widget build(BuildContext context) { // 5
  return MaterialApp(
    home: Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            RaisedButton(
              child: Text('Start Animation'),
              onPressed: () => startAnimation(),
            ),
            SizedBox(
              // アニメーションオブジェクトから現在の状態を取得して利用する
              width: animation.value,
              height: animation.value,
              child: Container(color: Colors.black),
            ),
          ],
        ),
      ),
    ),
  );
}

まとめ

アニメーション起動の流れとしては以下の通りです。

  1. ウィジェット作成時に initState() でアニメーションが初期化設定される
  2. ボタン押下 => startAnimation() 実行
  3. controller によって animation の状態が更新される
  4. addListener で登録されたリスナーに通知される
  5. 通知を受け取って setStatus() が呼ばれる
  6. 描画が更新されて最新の animation.value が反映される
  7. 4~6をアニメーション終了まで繰り返す

要するにアニメーションコントローラーによっていい感じに書き換えられた状態を描画することで、アニメーションが実現されているということです。

以上。

Flutterカテゴリの最新記事