AnimationControllerTweenCurve を使って、アニメーションを実装します。

複雑なアニメーションに挑戦しよう

今回は、以下のようなちょっと複雑なアニメーションを書いてみようと思います。

  1. キャラクターの画像が徐々に見えてくる
  2. キャラクターの画像がどんどん大きくなっていく
  3. 真ん中で画像を切り替わりながら、左右に行ったり来たりする

直線グラフのアニメーション

最初の例では、表示されるキャラクターの画像が徐々に見えてくるアニメーションの実装例を紹介します。

ここでは、 アニメーションの制御を行う AnimationController を設定してアニメーションを作っていきます。

アニメーションは値などに変化を加えて実現するため、動的に状態を更新する StatefulWidget を使います。 状態を更新しない静的な StatelessWiget では作れないので注意してください。

最終的に作るアニメーションは、以下のようになります。

AnimationController の初期化

AnimationController を使うためには、まず初期化する必要があります。

初期化する場所は、 StatefulWidget が作られるタイミングで処理してくれる initState メソッド内で行うことが一般的です。

@override
void initState() {
  animationController =
      AnimationController(
        duration: const Duration(seconds: 2),
        vsync: this,);
  animationController.addListener(() {
    setState(() {});
  });
  animationController.repeat();
  super.initState();
}

AnimationController の引数として、duration にアニメーション開始から終了までの時間、 vsyncthis を入れる形が基本となります。

ここの例では、 Duration に 2 秒を指定します。

StateAnimationController と連携させ、 vsyncthis を入れる場合は、事前に SingleTickerProviderStateMixinTickerProviderStateMixin を取り込む必要があります。 今回は、 AnimationController が単一なので、 SingleTickerProviderStateMixin を使います。使用するクラスに、 with を使って取り込みます。具体的な例は、全体のコードから参照してください。

次に .addListener() メソッドの中で、 setState(() {}) する必要があります。

これによって、アニメーションの状態が変化するたびに、アニメーションを保持する State を更新させることができます。

今回は、繰り返しのアニメーションとして表示させたいので、 animationController.repeat() を使います。

AnimationController の引数には他にも、初期値 value 、 下端 lowerBound 、上端 upperBound を指定できます。 ですが、基本的に、間違いを減らすために初期値のまま使うのがよいでしょう。

AnimationController の value を利用する

AnimationController は、 指定した Duration の間に、 0.0 から 1.0 までの範囲の数を生成します。

この生成された数の値は、 value プロパティから取得できます。

この範囲の値と、包む小要素の透過させる Opacity Widget を組み合わせて、キャラクターの画像が徐々に見えてくるアニメーションを実現します。

子要素の透過度を指定する opacity プロパティ に animationController.value を渡してあげるだけです。

これによって、 Duration で設定した 2 秒の間に、 opacity は 0.0 から 1.0 という範囲内で変化することとなります。

Opacity(
  opacity: animationController.value,
  child: Image.network(
    'https://nzigen.com/flutter-reference/assets/img/samples/kit-jumper-0002.png',
    width: 300,
    height: 300,
  ),
),

AnimationController の 破棄

State が破棄される際に、必ず animationController.dispose() しないといけません。

破棄しない場合、使われなくなった animationController は、メモリ上から解放されずにそのまま残ってしまいます。 忘れずにやりましょう。

@override
void dispose() {
  if (animationController != null) {
    animationController.dispose();
    animationController = null;
  }
  super.dispose();
}

全体のコードは、以下のようになります。

class LinearAnimationPage extends StatefulWidget {
  @override
  _LinearAnimationPageState createState() => _LinearAnimationPageState();
}

class _LinearAnimationPageState extends State<LinearAnimationPage>
    with SingleTickerProviderStateMixin {
  AnimationController animationController; // AnimationController のインスタンスを作成

  @override
  void initState() {
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    animationController.addListener(() {
      setState(() {});
    });
    animationController.repeat();
    super.initState();
  }

  @override
  void dispose() {
    if (animationController != null) {
      animationController.dispose();
      animationController = null;
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Opacity(
          opacity: animationController.value,
          child: Image.network(
            'https://nzigen.com/flutter-reference/assets/img/samples/kit-jumper-0002.png',
            width: 300,
            height: 300,
          ),
        ),
      ),
    );
  }
}

始まりと終わりがある直線グラフのアニメーション

次の例では、キャラクターの画像がどんどん大きくなっていくアニメーションの実装例を紹介します。

前回の例で使用した AnimationController 以外に、今回の例では、 Tween も使います。

Tween の初期化

前回の例では、 0.0 から 1.0 までの範囲の数を生成する AnimationController の値を使って、アニメーションを作りました。

もし異なる範囲の数を自由に生成したい場合には、 Tween がとても便利です。

Tween とは英語の Between の略称であり、指定する値 A と B の範囲で動くアニメーションを作ってくれるイメージです。

animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
animation = Tween<double>(begin: 10, end: 300).animate(animationController);

Tween の引数として、begin にアニメーション開始時の値、 end にアニメーション終了時の値を入れます。

そして、設定した Tween オブジェクトを使うために、 animate メソッドを呼びます。

この animate メソッドには、アニメーションの制御を行う animationController を渡します。

上記のコード例では、 2 秒間の間に 10 ~ 300 まで変動する値を生成してくれます。

全体のコードは、以下のようになります。

class tweenAnimationPage extends StatefulWidget {
  @override
  _tweenAnimationPageState createState() => _tweenAnimationPageState();
}

class _tweenAnimationPageState extends State<tweenAnimationPage>
    with SingleTickerProviderStateMixin {
  AnimationController animationController;
  Animation<double> animation;

  @override
  void initState() {
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    animation =
        Tween<double>(begin: 10, end: 300).animate(animationController);
    animation.addListener(() {
      setState(() {});
    });
    animationController.repeat();
    super.initState();
  }

  @override
  void dispose() {
    if (animationController != null) {
      animationController.dispose();
      animationController = null;
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Image.network(
          'https://nzigen.com/flutter-reference/assets/img/samples/kit-jumper-0002.png',
          width: animation.value,
          height: animation.value,
        ),
      ),
    );
  }
}

曲線グラフのアニメーション

次の例では、真ん中で画像を切り替わりながら、左右に行ったり来たりするアニメーションの実装例を紹介します。

Curve の実装

今回は、バウンスとかイージングを実現するための Curve も自前で実装してみます。

この実装には、三角関数を使う必要があり、様々な数学式が使えるライブラリをインポートする必要があります。

使いたい .dart ファイル上部に以下のように記載します。

import 'dart:math' as math;

インポートするライブラリの後ろに as ~~~ と追記することで、ライブラリに名前をつけます。そして、ライブラリの中の関数やクラスにアクセスする際のプレフィックスとして機能します。 (例: math.max , math.min , math.sqrt など)

以下のコード例では、円周率の値を定義した math.pi と、三角関数 math.sin の関数を使って、t0.0 から 1.0 動く間に、一回だけ行って帰って元の位置に戻ってくるようなカーブが実現できます。

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return 64 * math.sin(2 * math.pi * t) ;
  }
}

sin 関数は、 0.0 から始まり、 1.0 、0.0 、 -1.0 そして 0.0 に戻ってくる波状の滑らかな関数です。今回、それに、 64.0 を掛けることで振れ幅 (振幅) を実現しています。

上記の例では、 Curve を自作してみましたが、 Flutter にはよく使われるアニメーションカーブが豊富に定義されています。

Curves クラスで用意されているアニメーションカーブの一覧に目を通してみてください。

Curves class - animation library - Dart API

CurvedAnimation の初期化

これまでに実装してきたように、まず AnimationController を初期化します。

CurvedAnimation の引数として、parent にアニメーションさせたいもの、 curve に使いたいアニメーションカーブを渡します。

ここの例では、使いたいアニメーションカーブに自前で作った ShakeCurve() を使います。

@override
void initState() {
  animationController = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,
  );
  animationController.addListener(() {
    setState(() {});
  });
  animationController.repeat();
  curvedAnimation =
      CurvedAnimation(parent: animationController, curve: ShakeCurve());
  super.initState();
}

描画部分

描画部分にて、 curvedAnimation.value を使用して、位置と、画像の切り替えを実現します。

変数 left は、 Positionedleft プロパティに渡すものです。

Stack Widget を ConstrainedBox Widget で包むことによって矢印の動く範囲を調整します。

Stack 直下に、 Positioned を入れて、位置を可変にします。 Positionedchild に後で説明する image() を入れて完成です。

@override
Widget build(BuildContext context) {
  double left = (320.0 - 128.0) / 2.0;
  left += curvedAnimation.value;
  return ConstrainedBox(
    constraints: BoxConstraints.expand(width: 320.0, height: 128.0),
    child: Stack(
      fit: StackFit.expand,
      children: <Widget>[
        Positioned(
          left: left,
          top: 0.0,
          child: image(curvedAnimation.value),
        ),
      ],
    ),
  );
}

StackPositioned の間には、なにも入れてはいけません。 Positioned の親要素は、必ず Stack になるようにしましょう。

画像の切り替え

curvedAnimation.value を適用することによって、渡ってきた value を見て適切な画像を出力します。

Widget image(double value) {
  if (value >= 0) {
    return Icon(Icons.arrow_forward, size: 128.0,);
  }
  return Icon(Icons.arrow_back, size: 128.0,);
}

このようなコードになります。value が 0 以上であれば、右向きの矢印。 それ以外であれば、左向きの矢印を表示するようにしています。

全体のコードは、以下のようになります。

import 'dart:math' as math;

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return 64 * math.sin( 2 * math.pi * t) ;
  }
}

class FingerWidget extends StatefulWidget {
  @override
  _FingerWidgetState createState() =>
      _FingerWidgetState();
}

class _FingerWidgetState extends State<FingerWidget>
    with SingleTickerProviderStateMixin {
  AnimationController animationController;
  CurvedAnimation curvedAnimation;

  @override
  void initState() {
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    animationController.addListener(() {
      setState(() {});
    });
    animationController.repeat();
    curvedAnimation =
        CurvedAnimation(parent: animationController, curve: ShakeCurve());
    super.initState();
  }

  @override
  void dispose() {
    if (animationController != null) {
      animationController.dispose();
      animationController = null;
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    double left = (320.0 - 128.0) / 2.0;
    left += curvedAnimation.value;
    return ConstrainedBox(
      constraints: BoxConstraints.expand(width: 320.0, height: 128.0),
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Positioned(
            left: left,
            top: 0.0,
            child: image(curvedAnimation.value),
          ),
        ],
      ),
    );
  }

  Widget image(double value) {
    if (value >= 0) {
      return Icon(Icons.arrow_forward, size: 128.0,);
    }
    return Icon(Icons.arrow_back, size: 128.0,);
  }
}

参考リンク