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?
Introducing AnimatedWidgets
Adding many AnimatedBuilder
s to your build()
can make things seem a bit messy. Instead, you can use AnimatedWidget
to separate the different parts of your UI.
Make the following changes to SunWidget
:
// 1
class SunWidget extends AnimatedWidget {
// 2
const SunWidget({Key? key, required Animation<Offset> listenable})
: super(key: key, listenable: listenable);
// 3
Animation<Offset> get _animation => listenable as Animation<Offset>;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final minSize = min(constraints.maxWidth, constraints.maxHeight);
final sunSize = minSize / 2.0;
return Transform.translate(
// 4
offset: _animation.value,
child: Container(
width: sunSize,
height: sunSize,
decoration: BoxDecoration(
// …
),
),
);
});
}
}
In the code above, you:
- Extend
AnimatedWidget
instead ofStatelessWidget
. - Give
super
constructor yourListenable
(Animation
). - Cast
Listenable
asAnimation
for later use. - Use the current animation value in your
build()
method. It triggers each timeListenable
updates.
Now, do the same for MoonWidget
:
class MoonWidget extends AnimatedWidget {
const MoonWidget({Key? key, required Animation<Offset> listenable})
: super(key: key, listenable: listenable);
Animation<Offset> get _animation => listenable as Animation<Offset>;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final minSize = min(constraints.maxWidth, constraints.maxHeight);
final moonSize = minSize / 2.0;
return Transform.translate(
offset: _animation.value,
child: Container(
// Rest of MoonWidget
),
);
});
}
}
MoonWidget
works in the same way as SunWidget
, but the Transform.translate
child
changes.
Finally, replace _sunOrMoon()
in home_page.dart with the following:
return Stack(
children: [
SunWidget(listenable: _sunMoveAnim),
MoonWidget(listenable: _moonMoveAnim),
],
);
Instead of using AnimatedBuilders
, you directly use your AnimatedWidgets
.
Hot reload and launch the animation.
The animation looks the same, but it’s different below the hood. :]
Unlike AnimatedBuilder
, AnimatedWidget
doesn’t have a child
property to optimize its build()
. However, you could add it manually by adding an extra child
property, as in this example class:
class AnimatedTranslateWidget extends AnimatedWidget {
const AnimatedTranslateWidget(
{Key? key,
required Animation<Offset> translateAnim,
required Widget child})
: _child = child,
super(key: key, listenable: translateAnim);
// Child optimization
final Widget _child;
Animation<Offset> get animation => listenable as Animation<Offset>;
@override
Widget build(BuildContext context) {
return Transform.translate(
offset: animation.value,
child: _child,
);
}
}
The resulting class is an animated version of Transform.translate()
, just like SlideTransition
. You use it with your non-animated widgets as argument.
There’s a full list of widgets that have an AnimatedWidget
version. You name them according to the format FooTransition
, where Foo
is the animation’s name. You can use them and still control your animations because you pass them Animation
objects.
Implementing AnimatedWidget
If the animation you want to achieve is not very heavy and it’s OK for you to skip child
optimization, you may make your widget implement AnimatedWidget
. Here, you can use the animated versions of SunWidget
and MoonWidget
without it, for instance.
However, test it on your low-end target devices to be sure it runs well before putting it in production.
You may also want to achieve a special kind of animation that doesn’t already exist in the framework. In this case, you could create a widget implementing AnimatedWidget
to animate the components of your app. For instance, you could combine a fade effect with a translate effect and create a SlideAndFadeTransition
widget.
Animating Daytime and Nighttime Transition
Now that you know the theory, you can apply it to make your day/night transition! You already have animations for the sun and the moon, but they don’t depend on the current theme: The sun will always leave and the moon will always enter.
Start by updating _initThemeAnims()
to change that:
void _initThemeAnims({required bool dayToNight}) {
final disappearAnim =
Tween<Offset>(begin: const Offset(0, 0), end: Offset(-widget.width, 0))
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.0,
0.3,
curve: Curves.ease,
),
));
final appearAnim =
Tween<Offset>(begin: Offset(widget.width, 0), end: const Offset(0, 0))
.animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.7,
1.0,
curve: Curves.ease,
),
));
_sunMoveAnim = dayToNight ? disappearAnim : appearAnim;
_moonMoveAnim = dayToNight ? appearAnim : disappearAnim;
}
Instead of using raw values for the offset, you used widget.width
here to make sure you have a good animation, no matter what the screen size is. Also, instead of directly defining the sun and moon animations, you set appearAnim
and disappearAnim
. Then, you assign them to _sunMoveAnim
and _moonMoveAnim
depending on the animation you need to perform — day to night or night to day.
Interval
s don’t follow each other because you’ll add more animations between them.
Next, replace the contents of _sunOrMoon()
with this code:
if (_isDayTheme) {
return SunWidget(listenable: _sunMoveAnim);
} else {
return MoonWidget(listenable: _moonMoveAnim);
}
You return only one widget, depending on the current theme.
Now, you need to be able to change the theme. You’ll listen to _animationController
for this.
Update switchTheme()
and add the necessary methods below:
void _switchTheme() {
// 1
if (_isDayTheme) {
_animationController.removeListener(_nightToDayAnimListener);
_animationController.addListener(_dayToNightAnimListener);
} else {
_animationController.removeListener(_dayToNightAnimListener);
_animationController.addListener(_nightToDayAnimListener);
}
// 2
_initThemeAnims(dayToNight: _isDayTheme);
// 3
setState(() {
_animationController.reset();
_animationController.forward();
});
}
void _dayToNightAnimListener() {
_animListener(true);
}
void _nightToDayAnimListener() {
_animListener(false);
}
void _animListener(bool dayToNight) {
// 4
if ((_isDayTheme && dayToNight || !_isDayTheme && !dayToNight) &&
_animationController.value >= 0.5) {
setState(() {
_isDayTheme = !dayToNight;
});
}
}
Here’s what’s happening above:
- Remove the previous listener before adding the new one.
- Init again
Animation
objects with the new_isDayTheme
setting. - Refresh state with new
Animation
objects, then launch the animation from the start. - In the listener, eventually update
_isDayTheme
based on the current animation value.
Hot reload and click SWITCH THEMES.
Animating the Theme
You can animate the theme transition as well. Start by declaring the following Animation
below _moonMoveAnim
:
late Animation<ThemeData> _themeAnim;
Next, init it at the end of _initThemeAnims()
:
_themeAnim = (dayToNight
? ThemeDataTween(begin: _dayTheme, end: _nightTheme)
: ThemeDataTween(begin: _nightTheme, end: _dayTheme))
.animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(
0.3,
0.7,
curve: Curves.easeIn,
),
),
);
The code above interpolates between two ThemeData
s with ThemeDataTween
s. It’s another example of objects that need a dedicated Tween class.
Finally, replace the contents of build()
:
return AnimatedBuilder(
animation: _themeAnim,
child: _content(),
builder: (context, child) {
return Theme(
data: _themeAnim.value,
child: Builder(
builder: (BuildContext otherContext) {
return child!;
},
),
);
},
);
AnimatedBuilder
updates the Theme
of your HomePage
based on _themeAnim
‘s value.
Hot restart and launch the animation.
Now, Theme
‘s colors change progressively thanks to the animation.