Directional animations with built-in explicit animations
To watch this post in video form, check out our YouTube video here.
Hi! In our previous posts, we learned how to do some awesome animations using Flutter’s implicit animations. AnimatedFoo
and TweenAnimationBuilder
gave you the ability to drop some basic animations into your app. These animations typically go in one direction, “tweening” from a start to an end, where they stop. Behind the scenes, Flutter is taking control, assuming intentions and disposing of any need for you to worry about the transition from one thing to the next.
This works perfectly for many animation goals, but sometimes that ever-forward arrow of time leaves us feeling temporally locked. So, as we pause and contemplate the laws of thermodynamics and the inevitable heat death of the universe, wouldn’t it be nice if we could reverse time, and do it all again?
Enter our first foray into Flutter’s explicit animations! We won’t be building any time machines today, but we will be learning how to gain a bit more control over your animations using Transition widgets.
Transition widgets are a set of Flutter widgets whose names all end in — you guessed it —Transition. ScaleTransition
, DecoratedBoxTransition
, SizeTransition
, and more. They look and feel a lot like our AnimatedBlah
widgets. PositionedTransition
, for example, animates a widget’s transition between different positions. This is much like AnimatedPositioned
, but there is one major difference: these Transition widgets are extensions of AnimatedWidget
. This makes them explicit animations.
But, what does that really mean for us as app developers? Let’s step through what makes these animations tick.
Here, we’ll be creating an animation of galactic proportions, using this starting image. But, in this initial unanimated state, it doesn’t feel very galactic. Our first quest: to mix in some rotation.
RotationTransition
as an example
The RotationTransition
widget is a handy one that takes care of all of the trigonometry and transformations math to make things spin. Its constructor only takes three things:
// [Most of] RotationTransition’s constructor
RotationTransition({
Widget child,
Alignment alignment,
Animation<double> turns,
})
First is a child —the widget we want to rotate. The galaxy fits, so we’ll put it there:
RotationTransition(
child: GalaxyFitz(),
alignment: null, /*TODO*/
turns: null, /*TODO*/
)
Next, we need to give RotationTransition
the point our galaxy rotates around. Our galaxy’s black hole is roughly in the middle of the image where we’d normally expect. So, we’ll give an alignment
of center, making all of our rotational math “aligned” to that point.
RotationTransition(
child: GalaxyFitz(),
alignment: Alignment.center,
turns: null, /*TODO*/
)
Last, what is this mysteriously named turns property? The API docs tell us this is… an Animation
?!? Weren’t we creating an animation?
Not to worry! This is part of what makes RotationTransition
, and all the other Transition widgets, an explicit animation. We could accomplish the same rotation effect with an AnimatedContainer
and a transform, but then we’d rotate once and then stop. With our explicit animations, we have control of time and can make it so that our galaxy never stops spinning.
The turns property expects something that gives it a value and notifies it when that value changes. An Animation<double>
is just that. For RotationTransition
, the value corresponds to how many times we’ve turned, or more specifically, the percentage of one rotation completed.
Creating an AnimationController
One of the easiest ways to get an Animation<double>
is to create an AnimationController
, which is a controller for an animation. This controller handles listening for ticks¹ and gives us some useful controls over what the animation is doing.
We’ll need to create this in a stateful widget because keeping a handle on the controller will be important in our not-too-distant future. Because AnimationController
also has its own state to manage, we initialize it in initState, and dispose of it in dispose.
There are two parameters we must give to AnimationController
’s constructor. The first is a duration, which is how long our ̶t̶i̶m̶e̶ ̶m̶a̶c̶h̶i̶n̶e̶ animation lasts. The whole reason we’re here is that we need an object to tell us how far along we are in a single rotation. By default, AnimationController
“emits” values from 0.0
to 1.0
. How many and how granular those values are depends on how long we want a single rotation to take. Fortunately, Dart gives us a Duration
class to use. For the sake of this demo, we should have the galaxy spinning somewhere between 5 seconds and 230 million years per rotation. How about 15 seconds per turn then?
_animationController = AnimationController(
duration: Duration(seconds: 15),
// TODO: finish constructing me.
);
The next required parameter is vsync
. If you’re here from the future, welcome back! We hope you already know everything about vsync. For those who came here from the past, we’ll just say that this is what gives Flutter a reference to the object to notify about changes. this
is that thing, and it needs to mix in some ticker provider code. A future post will dive into more detail about vsync and ticker providers.
If we left things at that, nothing much happens. That’s because we’ve been given a controller, but haven’t pushed any of its buttons! We want our galaxy to spin forever, right? For that, we’ll just ask the controller to continually repeat the animation.
_animationController = AnimationController(
duration: Duration(seconds: 15),
vsync: this,
)..repeat();
Finally, we can go back and replace that null we left lingering around, by passing the animation controller to the turns
parameter in our RotationTransition
.
RotationTransition(
child: GalaxyFitz(),
alignment: Alignment.center,
turns: _animationController,
)
And, although we now have an infinitely rotating galaxy, this still doesn’t quite feel like we have control of time. The galaxy just does its thing now, right? Don’t forget, though, we have a handle on a controller. Let’s make use of it.²
Making use of an AnimationController
Allowing anyone to control the galaxy seems a bit too permissive though, so I’m going to make it an easter egg. I’ll add a sibling to the galaxy that’s a simple button, hidden off in the corner, and I’ll pass it a reference to our controller, so that within its onTap
listener, we can stop or restart the animation.
The controller maintains — among other things — the status of the animation, which we can check and stop if we’re running or restart if we’re not. And, there you go! By using an animation controller, we’re able to control the animation on demand. But that’s not all you can do with the controller.
With it, you can also animate to (or backwards from) a specific value, fling the animation forward with a given velocity, or control multiple animations with the same controller.
This was just our first taste of explicit animations in Flutter. We saw how a Transition widget works with AnimationController
, to provide some directionality and control over how our animation works. In future posts, we’ll be diving deeper into explicit animations and how to get even more customized.
- Galactic ticks are hard to hear, but AnimationController and TickerProviders help.
- As with all of your time controllers: use responsibly.
Articles in this series:
- How to Choose Which Flutter Animation Widget is Right for You?
- Flutter animation basics with implicit animations
- Custom Implicit Animations in Flutter…with TweenAnimationBuilder
- Directional animations with built-in explicit animations (this article)
- When should I useAnimatedBuilder or AnimatedWidget?
- Animation deep dive