DEV Community 👩‍💻👨‍💻

Cover image for 【Flutter】Mastering Animation ~4. The confusing parts of animations~
heyhey1028💙🔥
heyhey1028💙🔥

Posted on

【Flutter】Mastering Animation ~4. The confusing parts of animations~

The confusing part of animations

Adding animations is not always necessary to create an app, but there are still plenty of opportunities to write them.

I have implemented animations quite some time. And still, every time I write, I am baffled by how to implement animations.

In this article, I'll try to sort out some of the areas that personally confuse me.

1. there are AnimatedXXX and XXXTransition

If you look for articles on animation, you will often see classes called AnimatedXXX and XXXTransition. These classes are used to simplify the handling of certain animations.

However, these classes have always baffled me.

For example, if I want a widget to fade in, I can use either AnimatedOpacity or FadeTransition to achieve a fade-in animation.

So what's the difference? And when should you use which?

ImplicitlyAnimatedWidget and AnimatedWidget

AnimatedXXX and XXXTransition classes support a variety of animations other than fade-in animations, and each of them has a corresponding class in both classes.

These two groups of classes inherit from separate parent classes, ImplicitlyAnimatedWidget and AnimatedWidget.

ImplicitlyAnimatedWidget is inherited by the AnimatedXXX class group. And AnimatedWidget is inherited by the XXXTransition class group.

Since "classes that inherits ImplicitlyAnimatedWidget" and "classes that inherits AnimatedWidget" are too long, we call them Implicit widgets and Transition widgets, respectively.

(Google's engineers also called them this way, so it should be reasonable.

Please keep in mind that these are not the actual class names.

How are they different?

Difference between the two groups is simply whether or not they contain an AnimatedController.

Implicit widgets holds AnimationController within, while Transition widgets requires AnimationController to be passed as an argument.

Let's write a widget that changes scale in both class

Implicit widgets

// define variable for Implicit widget
  double implicitScale = 1;

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:
        ...
        // Implicit widget
        AnimatedScale(
          scale: implicitScale, // <<< passing variable
          duration: const Duration(seconds: 1),
          child: const Text("Hi, I'm Implicit"),
        ),
        ...
          // change the variable passed to Implicit widget
          onPressed: () {
            if (implicitScale == 1) {
              setState(() => implicitScale = 3);
              return;
            }
            setState(() => implicitScale = 1);
          },
          ...
Enter fullscreen mode Exit fullscreen mode

In Implicit widgets, you pass the variable you want to change (scale in the above example) and duration.

When you change the value of the variable to other value, the widget will animate the widget from starting value to changed value with defined duration.

It's that simple.

Transition widgets

  // define Animation which will be binded to Transition widget
  late AnimationController explicitController;
  late Tween<double> explicitTween;
  late Animation<double> explicitAnimation;

  @override
  void initState() {
    // create Animation for Transition widget
    controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);
    explicitTween = Tween(begin: 1, end: 3);
    explicitAnimation = explicitTween.animate(controller);
    super.initState();
  }

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:
        ...
        // Transition widget 
          ScaleTransition(
            scale: explicitAnimation, // <<< bind Animation created
            child: const Text("Hi, I'm Explicit"),
          ),
        ...
          // controller Animation class binded to Transition widget
          onPressed: () {
            if (!controller.isCompleted) {
              controller.forward();
              return;
            }
            controller.reverse();
          },
          ...
Enter fullscreen mode Exit fullscreen mode

In Transition widgets, Animation is created from AnimationController and Tween just as previous examples. It is passed as an argument.

Similarly, AnimationController is manipulated to fire the animation.

When to use which?

Now that you know the various classes involved in animation, how do you know which one to use?

It all depends on how complex you want to make your animation.

The following diagram, which is from two years ago, was created by an engineer at Google.

Screen Shot 2022-11-05 at 22 35 33

reference:https://www.youtube.com/watch?v=PFKg_woOJmI

The more you go from left to right, the customization freedom and complexity rises.

Explicit widgets in the diagram represents a use case where defining your own AnimationController, Tween, Curve and Animation and wrap them in an AnimationBuilder.

Use Implicit widgets

1.You want to animate to multiple values

Implicit widgets will do a nice job of animating from the current value to the new value you pass in.

You can change the value as many times as you want, as long as you change the value.

This is more flexible than using a Tween where you have to define the end value in advance.

2. For animations that only forwards

Implicit widgets are simpler because you do not need to manipulate the AnimationController. But that means they do not allow for complex operations that can be performed with the AnimationController.

Therefore, it is not possible to "forward and reverse" or repeat animation with a single trigger.

Having said that, Implicit widgets is more suitable to animation that animates from the start value to the end value only once.

Use Transition widgets

1.You want to create complex animations using TweenSequence or Interval

Transition widgets allow you to define your own Animation, so you can freely define a sequential animation using TweenSequence or a chain of animations using Interval.

You cannot define such complex animations with Implicit widgets.

2.You want to do complex animation operations such as repeat

As mentioned above, Implicit widgets can only perform forward operation, so you need to use Transition widgets if you want to perform repeat or forward and reverse animation operations.

Explicit widgets

For more complex animations
In addition to the above, if you want to add multiple animation effects to a widget, it is better to implement in Explicit ways, preparing AnimationController, Tween, Curve and Animation by yourself.
Otherwise you will need to wrap it with multiple Transition widgets, which will reduce readability.

2. You can generate Animation with both drive() and animate()

In my article, I have used the drive method of AnimationController to generate Animation, but you can also generate Animation by passing AnimationController to the animate method of Tween.

In other words, you can generate Animation from both the AnimationController side and the Tween side.

// create with AnimationController.drive
Animation = AnimationController.drive(Tween);

// create with Tween.animate
Animation = Tween.animate(AnimationController);
Enter fullscreen mode Exit fullscreen mode

This was very confusing when learning animations for the first time.

When to use which?

Two methods returns exactly the same result (Animation), so honestly, it doesn't matter whichever you use.

When considering, it should focus on "How many AnimationController and Tween are needed to achieve the desired animation?"

For example, the following code applies multiple animation effects to a single 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(),
      ),
    );
  }
 ),
 ...

Enter fullscreen mode Exit fullscreen mode

In such a case, since there are multiple Tweens for one AnimationController,
using animate method will make it easier to see from which tween, Animation is generated from.

On the other hand, if you want to apply same animation effect to two separate widgets, I would use the drive method of the AnimationController which make it easier to see from which AnimationController, Animation is generated from.

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(),
        )
      ]
    );
  }
 ),
 ...
Enter fullscreen mode Exit fullscreen mode

On the other hand, when AnimationController and Tween are one-to-one, you can use either one.

However, the above is just my personal opinion, so you can decide which one to use according to your preference.

3. Similar classes

The existence of similar classes are another reason why animation can be confusing.

For example

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

As you can see from the names, classes seems to be a combination of one of the main animation players such as Animation, Curve, Tween, etc.

It can easily get confusing understanding the difference and how and when to use which class.

For example, the following codes are written in different ways, but they all achieve the same animation.

Example using 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(),
    );
  } 
 ),
 ...
Enter fullscreen mode Exit fullscreen mode

Example using 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(),
    );
  } 
 ),
 ...
Enter fullscreen mode Exit fullscreen mode

Example using TweenAnimationBuilder


 ...
 TweenAnimationBuilder<double>(
  tween: CurveTween(curve: Curve.ease),
  duration: Duration(seconds: 1),
  builder:(context,opacity,_){
    return Opacity(
      opacity: opacity,
      child: Container(),
    );
  } 
 ),
 ...
Enter fullscreen mode Exit fullscreen mode

You would also find some examples which passes AnimationController directly, instead of passing Animation.

final controller = AnimationController(duration: Duration(seconds: 1),vsync:this);

 ...
 AnimationBuilder(
  animation:controller,
  builder: (context,_){
    return Opacity(
      opacity: controller.value,
      child: Container(),
    );
  }
 ),
 ...
Enter fullscreen mode Exit fullscreen mode

Looking at all these different ways of writing animations, it is easy to get confused about how the classes are related to each other.

In such a case, it is recommended to classify each class by what parent class it inherits from.

image

By looking at the diagram, you would understand that AnimationController sometimes is passed directly as an animation because it inherits Animation. Also, you might understand why CurveTween can be passed as a tween, and why TweenAnimationBuilder doesn't need to be passed when using AnimationController.

Also, since AnimationController has a value that changes from 0 to 1 by default, it can be used as an Animation of Tween(begin:0,end:1), so you can pass it as an animation parameter without having to tie a tween to it. can be used as Animation of (begin:0,end:1).

Sample Repository

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

That's it!!

Because there are so many different classes, Animations can be overwhelming at first. But if you understanding the things described in this article, I think you will get much familiar with animations.

I hope these series of articles will help you understand animation a little better.

Happy dev life with Animation!!!!

Reference

Top comments (0)

Why You Need to Study Javascript Fundamentals

The harsh reality for JS Developers: If you don't study the fundamentals, you'll be just another “Coder”. Top learnings on how to get to the mid/senior level faster as a JavaScript developer by Dragos Nedelcu.