【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. 似てるクラスが沢山ある
似たようなクラスが多く存在するのもアニメーションが分かりづらいと感じる一因でしょう
例えば
AnimationBuilder
TweenAnimationBuilder
Curves
Animation
Tween
CurvedAnimation
CurveTween
命名から察するに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