Getting Started With Staggered Animations in Flutter
Animations in mobile apps are powerful tools to attract users’ attention. They make transitions between screens and states smoother and more appealing for the user. In this tutorial, you’ll learn how to implement animations in Flutter. By Sébastien Bel.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With Staggered Animations in Flutter
30 mins
- Getting Started
- Animating Goodnightmoonhellosun
- Using Implicit Animations
- Applying Tween Animations
- Implementing Curve Animations
- Animating the Sun and the Moon Implicitly
- Using Explicit Animations
- Animating With Animation
- AnimatedBuilder
- Coordinating Animations With Intervals
- Introducing AnimatedWidgets
- Implementing AnimatedWidget
- Animating Daytime and Nighttime Transition
- Animating the Theme
- Animating the TodayDetails Widget
- Interpolating a Custom Object
- Adding More Animations
- Add an Animation to BottomCard
- Make a Startup Animation
- Improve CloudyWidget
- Make It Rain or Snow
- Solution
- Where to Go From Here?
Using Explicit Animations
Staggered animations are animations that follow or overlap each other. You’ll use explicit animations to implement them.
AnimationController
allows you to control animations. You can forward()
, reverse()
, repeat()
, reset()
and stop()
animations linked to it. Check the doc for more details about AnimationController
.
You must use a mixin to create instances of AnimationController
. Which mixin you should use depends on the number of AnimationController
s:
- Use a
SingleTickerProviderStateMixin
if you have oneAnimationController
. - Use a
TickerProviderStateMixin
if you have two or moreAnimationController
s.
Edit _HomePageState
located at lib/ui/home_page.dart:
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
// …
}
Because you’ll use only one AnimationController
, you added SingleTickerProviderStateMixin
.
Now, initialize AnimationController
in initState()
by adding the following above didChangeDependencies()
:
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
);
}
With that, you added AnimationController
, which is three seconds in duration. Its vsync
parameter needs TickerProvider
. Here’s where you use the mixin.
_animationController
needs one more thing. Copy and paste the following in the same class:
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Now, you dispose _animationController
with dispose()
when you don’t need it anymore.
Finally, replace the contents of _switchTheme()
with the following:
_animationController.reset();
_animationController.forward();
This resets _animationController
to start again from zero if it had been previously started and starts the animation again.
Now that you can control animations, you need actual animation objects!
Animating With Animation
Animation
gives the current status
and value
of Tween
interpolation, as well as letting you listen to their changes. You’ll use that to animate your widgets. You usually use AnimationController
s to control to control Animation
s.
Start by declaring Animation
for the sun and the moon below the _animationController
declaration:
late Animation<Offset> _sunMoveAnim;
late Animation<Offset> _moonMoveAnim;
Next, you need to initialize them. The simplest way is to call animate()
on your Tween
. Do this in a new method:
void _initThemeAnims({required bool dayToNight}) {
_sunMoveAnim =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(-500, 0))
.animate(_animationController);
_moonMoveAnim =
Tween<Offset>(begin: const Offset(500, 0), end: const Offset(0, 0))
.animate(_animationController);
}
animate()
takes Animation
as a parameter, but here you use _animationController
instead. You can do this because AnimationController
inherits from Animation
. The newly created method takes dayToNight
as an argument; you’ll get back to this in a moment.
Since you use animate using _animationController
, the animation duration will be the same as _animationController
‘s duration. You defined that value when you initialized your _animationController
: 3,000 milliseconds.
With the above code, _animationController
animates both _sunMoveAnim
and _moonMoveAnim
at the same time, each with its own Tween
.
AnimationController
animation value goes from the lowest to the highest value, which are 0.0 and 1.0 if you didn’t override them. You can also use AnimationController
as Animation
directly instead of creating another Animation
.
Next, add the following at the bottom of didChangeDependencies()
:
_isDayTheme = Theme.of(context).brightness == Brightness.light;
_initThemeAnims(dayToNight: _isDayTheme);
Here, you initialize the theme based on the device’s theme brightness. Then, you use it to initialize your Animation
s.
Animation
is listenable; in other words, you can attach listeners to them. Animation
notifies the status listener when its AnimationStatus
changes by using addStatusListener()
:
exampleAnim.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print('completed');
}
});
This example prints a message when the Animation
completes.
On the other hand, addListener()
listens to all value changes. Here’s how to use it:
exampleAnim.addListener(() {
print('exampleAnim value: ${exampleAnim.value}');
});
This code snippet prints the current, updated value each time exampleAnim.value
changes. You could also call setState()
from there to update your widgets, but that’s not the recommended way.
AnimatedBuilder
You usually use StatefulWidget
and setState()
to update your widget tree. Yet, AnimatedBuilder
simplifies this process for Animations
.
It has three parameters:
-
child: The non-moving part of the animation.
AnimatedBuilder
builds it once instead of rebuilding it each time the animation changes. -
animation:
Listenable
you’re listening to. - builder: The part that changes with the animation.
Replace the contents of _sunOrMoon()
with the following:
return Stack(
children: [
// 1
AnimatedBuilder(
// 2
child: const SunWidget(),
// 3
animation: _sunMoveAnim,
// 4
builder: (ctx, child) {
return Transform.translate(
// 5
offset: _sunMoveAnim.value,
child: child,
);
},
),
// 6
AnimatedBuilder(
child: const MoonWidget(),
animation: _moonMoveAnim,
builder: (ctx, child) {
return Transform.translate(
offset: _moonMoveAnim.value,
child: child,
);
},
),
],
);
Here’s what’s happening in the code above:
- Use an
AnimatedBuilder
instead ofTweenAnimationBuilder
. - Set the non-moving part in
child
. - Define which
Animation
AnimatedBuilder
will listen to. Here, it’s_sunMoveAnim
. - Set
builder
to perform the actual animation. This one translateschild
.AnimatedBuilder
callsbuilder
each time the animation updates. - Get
Animation
‘s current value. - Do the same for the moon.
Hot restart and click SWITCH THEMES.
You now have your first explicit animation, but it plays linearly. Unlike when you use TweenAnimationBuilder
, you have to handle the curve yourself.
Replace _initThemeAnims()
content with the following:
_sunMoveAnim =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(-400, 0))
.animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
);
_moonMoveAnim =
Tween<Offset>(begin: const Offset(400, 0), end: const Offset(0, 0))
.animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
The _sunMoveAnim
and _moonMoveAnim
initializations apply easeIn
and easeOut
curves, respectively, thanks to CurvedAnimation
.
Hot restart and click the SWITCH THEMES button.
Since your animations run simultaneously, the sun and the moon can be visible at the same time. You’ll change that next.
Coordinating Animations With Intervals
You’ll use Interval
to define when you want each animation to start and end.
Replace the contents of _initThemeAnims()
with:
_sunMoveAnim =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(-400, 0))
.animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 0.5, curve: Curves.easeIn),
),
);
_moonMoveAnim =
Tween<Offset>(begin: const Offset(400, 0), end: const Offset(0, 0))
.animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
),
);
In the code above, you used Interval
as CurvedAnimation
Interval
can also have a curve
.
The resulting animations will play for a fraction of _animationController
‘s total time. _sunMoveAnim
will take the first half (0.0 to 0.5), while _moonMoveAnim
will play during the second half (0.5 to 1.0). Each will take 1,500 milliseconds since _animationController
is 3,000 milliseconds long.
Hot restart and launch the animation.