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);
},
...
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();
},
...
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.
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);
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(),
),
);
}
),
...
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(),
)
]
);
}
),
...
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(),
);
}
),
...
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(),
);
}
),
...
Example using TweenAnimationBuilder
...
TweenAnimationBuilder<double>(
tween: CurveTween(curve: Curve.ease),
duration: Duration(seconds: 1),
builder:(context,opacity,_){
return Opacity(
opacity: opacity,
child: Container(),
);
}
),
...
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(),
);
}
),
...
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.
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!!!!
Top comments (0)