AnimationController
、 Tween
、 Curve
を使って、アニメーションを実装します。
複雑なアニメーションに挑戦しよう
今回は、以下のようなちょっと複雑なアニメーションを書いてみようと思います。
- キャラクターの画像が徐々に見えてくる
- キャラクターの画像がどんどん大きくなっていく
- 真ん中で画像を切り替わりながら、左右に行ったり来たりする
直線グラフのアニメーション
最初の例では、表示されるキャラクターの画像が徐々に見えてくるアニメーションの実装例を紹介します。
ここでは、 アニメーションの制御を行う 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
にアニメーション開始から終了までの時間、 vsync
に this
を入れる形が基本となります。
ここの例では、 Duration
に 2 秒を指定します。
State
を AnimationController
と連携させ、 vsync
に this
を入れる場合は、事前に SingleTickerProviderStateMixin
か TickerProviderStateMixin
を取り込む必要があります。
今回は、 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() {
animationController.dispose();
super.dispose();
}
全体のコードは、以下のようになります。
class LinearAnimationPage extends StatefulWidget {
@override
_LinearAnimationPageState createState() => _LinearAnimationPageState();
}
class _LinearAnimationPageState extends State<LinearAnimationPage>
with SingleTickerProviderStateMixin {
late 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() {
animationController.dispose();
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 {
late 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() {
animationController.dispose();
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
の関数を使って、t
が 0.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
は、 Positioned
の left
プロパティに渡すものです。
Stack
Widget を ConstrainedBox
Widget で包むことによって矢印の動く範囲を調整します。
Stack
直下に、 Positioned
を入れて、位置を可変にします。
Positioned
の child
に後で説明する 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),
),
],
),
);
}
Stack
と Positioned
の間には、なにも入れてはいけません。
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 {
late 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() {
animationController.dispose();
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,);
}
}