🐎

【Flutter】Animationの基礎から応用まで ~④アニメーションのわかりづらい所~

2022/11/12に公開

アプリを作る上でアニメーションを追加する事は必ずしも必要ではありませんが、それでも書く機会はそこそこあると思います。

私自身もアプリをリッチにする為にUIほどではありませんが書いてきました。それでも毎回、アニメーションの実装に困惑する事があります。

今回は個人的に困惑の元となる部分を整理していきます

1. Animated〇〇と〇〇Transitionがある

アニメーションの記事を探してみると特定のアニメーションをシンプルに扱う為にAnimated〇〇〇〇Transitionというクラスがよく紹介されています

しかしこのクラス群は私にとっては困惑の種でした

例えばフェードインするWidgetを実現したい場合、AnimatedOpacityFadeTransitionのどちらを使ってもフェードインのアニメーションを実現出来ます

それではこれはどう違うのでしょう?そしてどう使い分けるのでしょう?

ImplicitlyAnimatedWidgetとAnimatedWidget

Animated〇〇〇〇Transitionはフェードインアニメーションの他にも様々なアニメーションに対応しており、それぞれ対応したクラスがどちらにも存在します

この2つのクラス群はそれぞれImplicitlyAnimatedWidgetAnimatedWidgetという別々の親クラスを継承しています

ImplicitlyAnimatedWidgetを継承するのがAnimated〇〇のクラス群、AnimatedWidgetを継承するのが〇〇Transitionのクラス群です

ImplicitlyAnimatedWidgetを継承するクラス」、「AnimatedWidgetを継承するクラス」だと長いので、ここではそれぞれImplicit widgetsTransition widgetsと呼びます

Googleのエンジニアもそう呼んでいたので良いはず

実際に存在するクラス名ではない事だけは留意しておいてください

どう違うの?

この2つのクラス群の違いはズバリ「AnimatedControllerを内包しているかどうか」です

Implicit widgetsAnimationControllerを内部で持っているのに対し、Transition widgetsAnimationControllerを引数として渡す必要があります

同じ様にスケールが変わる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では、従来通りAnimationControllerTweenからAnimationを生成し、引数として渡しています

同様にAnimationControllerを操作する事でアニメーションを発火しています

どう使い分けるのか?

アニメーションに関わる様々なクラスを知った所で、では結局どう使い分けたら良いのか?

それはひとえに どれくらい複雑なアニメーションを実現したいか? によります

2年前の資料ですがGoogleのエンジニアが整理した以下の図がわかりやすいと思います

参考元:https://www.youtube.com/watch?v=PFKg_woOJmI

左から右へ行くほどカスタマイズの自由度と対応できる複雑さが上がっていきます

図の中のExplicit widgetsAnimationControler,Tween,Curve,Animationを自前で定義し、AnimationBuilderでラップする事を指します

Implicit widgets

1.変化の値が可変の時

Implicit widgetsでは渡す値を変化させれば、現在の値から新しい値へのアニメーションをいい感じにやってくれます

値を変えれば良いだけなので、どんな値に何度でも変える事が出来ます

Tweenを使う場合は事前に終了の値を定義しなければならない事に比べると柔軟です

2.forwardさせるだけのアニメーションの時

Implicit widgetsではAnimationControllerを操作しない分、シンプルですが、その分、AnimationControllerで行える複雑な操作は行えません

その為、一度の発火で「再生(forward)してまた逆再生(reverse)させる」や繰り返し(repeat)するアニメーション操作に対応出来ません

逆に言えば、始まりの値から終了の値までを一度のみアニメーションするだけであれば、Implicit widgetsで十分かもしれません

Transition widgets

1.TweenSequenceIntervalを使った複雑なアニメーションをさせたい時

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としてAnimationControllerTweenCurveAnimationを用意するのが良いと思います

2. drive()とanimate()どっちでもAnimationが生成出来る

私の記事では、AnimationControllerdriveメソッドを使ってAnimationを生成してきましたが、実はTweenanimateメソッドにAnimationControllerを渡す事でもAnimationは生成できます

つまりAnimationController側からもTween側からもAnimationを生成する事が出来ます

// AnimationController起点
Animation = AnimationController.drive(Tween);

// Tween起点
Animation = Tween.animate(AnimationController);

これは特に最初は困惑する所かと思います。

どう使い分けるのか?

実際には2つのメソッドは全く同じ結果(Animation)を返してくれるので、正直どちらを使っても問題ありません

強いて言うならば 「希望のアニメーションを実現するにあたってAnimationControllerTweenがいくつ必要か」 で考えるのが良いと思います

例えば以下のコードでは複数のアニメーション効果を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(),
        )
      ]
    );
  }
 ),
 ...

一方、AnimationControllerTweenが1対1の時は正直どちらでも構いません

とはいえ以上はあくまでも個人的な見解なので、お好みでどっちを使うか決めて問題ありません

3. 似てるクラスが沢山ある

似たようなクラスが多く存在するのもアニメーションが分かりづらいと感じる一因でしょう

例えば

  • AnimationBuilder
  • TweenAnimationBuilder
  • Curves
  • Animation
  • Tween
  • CurvedAnimation
  • CurveTween

命名から察するにAnimationCurve,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(),
    );
  }
 ),
 ...

こういった様々なアニメーションの書かれ方を見ると各クラスの関係性がどうなっているのか頭の中がこんがらがってしまいます

そんな時は各クラスが何の親クラスを継承しているのかで分類してみる事をお勧めします

image

先ほどの例で言えば、AnimationControllerは実はAnimationを継承している為、animationに引数として渡す事が出来たり、なぜCurveTweenTweenとして渡せたり、なぜTweenAnimationBuilderを使ってる時はAnimationControllerを渡さなくて良いのかなどがこの図を見ると分かります

またAnimationControllerをデフォルトで0から1へ変化する値を持っているので、Tween(begin:0,end:1)Animationとして使える為、Tweenを紐づけること無くanimationパラメータに渡す事が出来ます

サンプルコード

https://github.com/heyhey1028/flutter_samples/tree/main/samples/master_animation

以上

アニメーションは様々なクラスが入り混じり、最初は非常に困惑しますが、この記事に書かれた様な事を理解しておけば少しはとっつきやすくなるかと思います

一連の記事が少しでもアニメーションの理解の一助になれば嬉しいです

Happy dev life with Animation!!!!

参考

Flutter大学

Discussion