【Flutter】Animationの基礎から応用まで ~④アニメーションのわかりづらい所~
アプリを作る上でアニメーションを追加する事は必ずしも必要ではありませんが、それでも書く機会はそこそこあると思います。
私自身もアプリをリッチにする為にUIほどではありませんが書いてきました。それでも毎回、アニメーションの実装に困惑する事があります。
今回は個人的に困惑の元となる部分を整理していきます
1. Animated〇〇と〇〇Transitionがある
アニメーションの記事を探してみると特定のアニメーションをシンプルに扱う為にAnimated〇〇や〇〇Transitionというクラスがよく紹介されています
しかしこのクラス群は私にとっては困惑の種でした
例えばフェードインするWidgetを実現したい場合、AnimatedOpacityとFadeTransitionのどちらを使ってもフェードインのアニメーションを実現出来ます
それではこれはどう違うのでしょう?そしてどう使い分けるのでしょう?
ImplicitlyAnimatedWidgetとAnimatedWidget
Animated〇〇や〇〇Transitionはフェードインアニメーションの他にも様々なアニメーションに対応しており、それぞれ対応したクラスがどちらにも存在します
この2つのクラス群はそれぞれImplicitlyAnimatedWidgetとAnimatedWidgetという別々の親クラスを継承しています
ImplicitlyAnimatedWidgetを継承するのがAnimated〇〇のクラス群、AnimatedWidgetを継承するのが〇〇Transitionのクラス群です
「ImplicitlyAnimatedWidgetを継承するクラス」、「AnimatedWidgetを継承するクラス」だと長いので、ここではそれぞれImplicit widgets 、Transition widgetsと呼びます
Googleのエンジニアもそう呼んでいたので良いはず
実際に存在するクラス名ではない事だけは留意しておいてください
どう違うの?
この2つのクラス群の違いはズバリ「AnimatedControllerを内包しているかどうか」です
Implicit widgetsはAnimationControllerを内部で持っているのに対し、Transition widgetsはAnimationControllerを引数として渡す必要があります
同じ様にスケールが変わるWidgetをそれぞれのクラスで書いてみましょう
Implicit widgets
// ImplicitlyAnimatedWidgteに渡す値を定義
double implicitScale = 1;
Widget build(BuildContext context) {
return Scaffold(
body:
...
// ImplicitlyAnimatedWidgetの本体
AnimatedScale(
scale: implicitScale, // <<< scaleの値を渡す
duration: const Duration(seconds: 1),
child: const Text("Hi, I'm Implicit"),
),
...
// ImplicitlyAnimatedWidgetに渡す値を変化させる
onPressed: () {
if (implicitScale == 1) {
setState(() => implicitScale = 3);
return;
}
setState(() => implicitScale = 1);
},
...
Implicit widgetsでは、変化させたい値(上記ではscale)と変化のスピードdurationを渡します
この渡している値を変化させると「変化前の値 → 変化後の値」まで渡したdurationでよしなにアニメーションしてくれます
シンプルですね
Transition widgets
// ExplicitAnimatedWidgteに渡す値を定義
late AnimationController explicitController;
late Tween<double> explicitTween;
late Animation<double> explicitAnimation;
void initState() {
// ExplicitAnimatedWidgetに渡すAnimationを生成
controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this);
explicitTween = Tween(begin: 1, end: 3);
explicitAnimation = explicitTween.animate(controller);
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
body:
...
// ExplicitAnimatedWidgetの本体
ScaleTransition(
scale: explicitAnimation, // <<< 生成したAnimationを渡す
child: const Text("Hi, I'm Explicit"),
),
...
// ExplicitAnimatedWidgetに渡しているAnimationを操作する
onPressed: () {
if (!controller.isCompleted) {
controller.forward();
return;
}
controller.reverse();
},
...
Transition widgetsでは、従来通りAnimationControllerとTweenからAnimationを生成し、引数として渡しています
同様にAnimationControllerを操作する事でアニメーションを発火しています
どう使い分けるのか?
アニメーションに関わる様々なクラスを知った所で、では結局どう使い分けたら良いのか?
それはひとえに どれくらい複雑なアニメーションを実現したいか? によります
2年前の資料ですがGoogleのエンジニアが整理した以下の図がわかりやすいと思います

参考元:https://www.youtube.com/watch?v=PFKg_woOJmI
左から右へ行くほどカスタマイズの自由度と対応できる複雑さが上がっていきます
図の中のExplicit widgetsはAnimationControler,Tween,Curve,Animationを自前で定義し、AnimationBuilderでラップする事を指します
Implicit widgets
1.変化の値が可変の時
Implicit widgetsでは渡す値を変化させれば、現在の値から新しい値へのアニメーションをいい感じにやってくれます
値を変えれば良いだけなので、どんな値に何度でも変える事が出来ます
Tweenを使う場合は事前に終了の値を定義しなければならない事に比べると柔軟です
2.forwardさせるだけのアニメーションの時
Implicit widgetsではAnimationControllerを操作しない分、シンプルですが、その分、AnimationControllerで行える複雑な操作は行えません
その為、一度の発火で「再生(forward)してまた逆再生(reverse)させる」や繰り返し(repeat)するアニメーション操作に対応出来ません
逆に言えば、始まりの値から終了の値までを一度のみアニメーションするだけであれば、Implicit widgetsで十分かもしれません
Transition widgets
1.TweenSequenceやIntervalを使った複雑なアニメーションをさせたい時
Transition widgetsではAnimationを自分で定義出来る分、TweenSequenceを使ったシークエンスアニメーションやIntervalを使った連鎖的なアニメーションを自由に定義出来ます
Implicit widgetsではこのような複雑なアニメーションは定義出来ません
2.repeatなど複雑なアニメーション操作をしたい時
前述の通り、Implicit widgetsではforward操作しか行えない為、「再生(forward)してまた逆再生(reverse)させる」や繰り返し(repeat)するアニメーション操作をしたい時はTransition widgetsを使う必要があります
Explicit widgets
さらに複雑なアニメーションをしたい時
上記に加え、複数のアニメーション効果を1つのwidgetに加えたい場合、複数のTransition widgetsでラップする事になり、可読性が下がってしまうので、その様な場合はExplicit widgetとしてAnimationController、Tween、Curve、Animationを用意するのが良いと思います
2. drive()とanimate()どっちでもAnimationが生成出来る
私の記事では、AnimationControllerのdriveメソッドを使ってAnimationを生成してきましたが、実はTweenのanimateメソッドにAnimationControllerを渡す事でもAnimationは生成できます
つまりAnimationController側からもTween側からもAnimationを生成する事が出来ます
// AnimationController起点
Animation = AnimationController.drive(Tween);
// Tween起点
Animation = Tween.animate(AnimationController);
これは特に最初は困惑する所かと思います。
どう使い分けるのか?
実際には2つのメソッドは全く同じ結果(Animation)を返してくれるので、正直どちらを使っても問題ありません
強いて言うならば 「希望のアニメーションを実現するにあたってAnimationControllerとTweenがいくつ必要か」 で考えるのが良いと思います
例えば以下のコードでは複数のアニメーション効果を1つのWidgetに適用しています
final controller = AnimationController(duration: Duration(seconds: 1),vsync:this);
final alignTween = Tween(begin: Alignment.topCenter,end: Alignment.bottomCenter);
final rotateTween = Tween(begin:0, end: pi *8);
final alignAnimation = alignTween.animate(controller);
final rotateAnimation = rotateTween.animate(controller);
...
AnimationBuilder(
animation:controller,
builder: (context, _){
return Align(
alignment: alignAnimation.value,
child: Transform.rotate(
angle: rotateAnimation.value,
child: Container(),
),
);
}
),
...
この様な場合、1つのAnimationControllerに対し、複数のTweenが存在するのでTween起点のanimateメソッドで記述しておくとどのTweenから生成したAnimationかが分かりやすくなります
一方、以下のように別々に動くWidgetに同じアニメーション効果を適用したい場合は、AnimatonController起点のdriveメソッドで記述しておくと、どちらのAnimationControllerの為に生成したAnimationかが分かりやすくなると思います
final controllerA = AnimationController(duration: Duration(seconds: 1),vsync:this);
final controllerB = AnimationController(duration: Duration(milliseconds: 500),vsync:this);
final offsetTween = Tween(begin: Offset(0,1), end:Offset.zero);
final animationA = controllerA.drive(offsetTween);
final animationB = controllerB.drive(offsetTween);
...
AnimationBuilder(
animation:Listenable.merge([
animationA,
animationB,
]),
builder: (context,_){
return Column(
children: [
Transform.translate(
offset: animationA.value,
child: WidgetA(),
),
Transform.translate(
offset: animationB.value,
child: WidgetB(),
)
]
);
}
),
...
一方、AnimationControllerとTweenが1対1の時は正直どちらでも構いません
とはいえ以上はあくまでも個人的な見解なので、お好みでどっちを使うか決めて問題ありません
3. 似てるクラスが沢山ある
似たようなクラスが多く存在するのもアニメーションが分かりづらいと感じる一因でしょう
例えば
AnimationBuilderTweenAnimationBuilderCurvesAnimationTweenCurvedAnimationCurveTween
命名から察するにAnimation、Curve,Tweenなどアニメーションの主要プレーヤーのどれかとどれかを掛け合わせたようなクラスが沢山あります
この様なクラスが沢山あるとその違いやどう使い分けたら良いのか、またどのクラスがどのクラスの子クラスなのか困惑してしまいますよね
例えば以下の3つのコードは書き方は違いますが、全て同じアニメーションを実現しています
CurvedAnimationを使った例
final controller = AnimationController(duration: Duration(seconds: 1),vsync:this);
final animation = CurvedAnimation(
parent: controller,
curve: const Curve.ease,
);
...
AnimationBuilder(
animation:animation,
builder:(context,_){
return Opacity(
opacity: animation.value,
child: Container(),
);
}
),
...
CurveTweenを使った例
final controller = AnimationController(duration: Duration(seconds: 1),vsync:this);
final animation = controller.drive(CurveTween(curve:Curves.ease));
...
AnimationBuilder(
animation:animation,
builder:(context,_){
return Opacity(
opacity: animation.value,
child: Container(),
);
}
),
...
TweenAnimationBuilderを使った例
...
TweenAnimationBuilder<double>(
tween: CurveTween(curve: Curve.ease),
duration: Duration(seconds: 1),
builder:(context,opacity,_){
return Opacity(
opacity: opacity,
child: Container(),
);
}
),
...
また以下の様にAnimationを渡す所をAnimationControllerを直接渡しているサンプルも見受けられます
final controller = AnimationController(duration: Duration(seconds: 1),vsync:this);
...
AnimationBuilder(
animation:controller,
builder: (context,_){
return Opacity(
opacity: controller.value,
child: Container(),
);
}
),
...
こういった様々なアニメーションの書かれ方を見ると各クラスの関係性がどうなっているのか頭の中がこんがらがってしまいます
そんな時は各クラスが何の親クラスを継承しているのかで分類してみる事をお勧めします

先ほどの例で言えば、AnimationControllerは実はAnimationを継承している為、animationに引数として渡す事が出来たり、なぜCurveTweenをTweenとして渡せたり、なぜTweenAnimationBuilderを使ってる時はAnimationControllerを渡さなくて良いのかなどがこの図を見ると分かります
またAnimationControllerをデフォルトで0から1へ変化する値を持っているので、Tween(begin:0,end:1)のAnimationとして使える為、Tweenを紐づけること無くanimationパラメータに渡す事が出来ます
サンプルコード
以上
アニメーションは様々なクラスが入り混じり、最初は非常に困惑しますが、この記事に書かれた様な事を理解しておけば少しはとっつきやすくなるかと思います
一連の記事が少しでもアニメーションの理解の一助になれば嬉しいです
Happy dev life with Animation!!!!
Discussion