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?
If you want your app to stand out from the crowd, you might be surprised at what a difference just a few animations make. Not only can animations bring your app to life, but they’re also a very simple way to explain what’s happening on the screen.
Luckily, Flutter makes the process of adding animations very easy. You can use implicit animations for simple cases, or you can choose explicit animations if you need more control.
Implicit animations are really easy to use, but they only allow you to control the duration and the curve of your animations. For example, use AnimatedFoo
instead of Foo
and your widget will animate automatically when its values change — and Container
becomes AnimatedContainer
. Find the full list for ImplicitlyAnimatedWidget
in the Flutter documentation.
When you need more control over the lifecycle of your animation, though, such as the ability to pause it or launch it on demand, you have to use explicit animations. They’re especially useful when there are several elements to coordinate.
In this tutorial, you’ll build a weather app called Good Night Moon Hello Sun. With the help of staggered animations, you’ll create a custom day-to-night animation using:
AnimationController
Tween
-
Tweens
withIntervals
-
AnimatedBuilder
andAnimatedWidget
to render the animations
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial and open it in your favorite IDE.
The code is in the lib folder, and the images are in assets/icons. Open main.dart and take a look at the home
widget given to MaterialApp
:
LayoutBuilder(builder: (context, constraints) {
return HomePage(width: constraints.maxWidth);
})
HomePage
uses LayoutBuilder
constraints to know its size and be more responsive than with hard-coded values.
Speaking of which, open home_page.dart.
Since you’re building a weather app, you need to give it some data. SampleDataGenerator
generates weather mock data using classes you’ll find in model:
- WeatherData: Weather for a day with a list of its details.
- WeatherDataDetails: Weather, temperature and wind at a given time of the day.
You usually change the theme of an app by changing MaterialApp
‘s theme, but MaterialApp
includes a default implicit animation that’s not customizable. So instead, you’ll use the Theme
widget — which is not animated by default — to fully control the animation between the day and night themes.
In home_page.dart, build()
only contains Theme
, which depends on _isDayTheme
because _content()
returns the rest of the widget tree. You’ll use _animationButton()
to launch the animation.
The rest of the widgets are pretty common, except for these custom ones:
SunWidget
MoonWidget
CloudyWidget
TodayDetailsWidget
BottomCard
Take a look at sun_widget.dart. This is your sun, which you’ll animate later. Note, again, the presence of LayoutBuilder
to set its size.
Next, open moon_widget.dart. You’ll see that MoonWidget
uses an image instead of just a Container
.
cloudy_widget.dart shows clouds on top of SunWidget
or MoonWidget
, depending on the theme.
today_details_widget.dart is where you display the temperature, wind and weather of the day.
Finally, open bottom_card.dart. BottomCard
displays both the hourly weather and the forecast for the next five days.
Now that you’re familiar with the project’s code, build and run.
Test switching the theme by clicking SWITCH THEMES; you’ll notice you have two different themes for your app, but no animations yet.
Animating Goodnightmoonhellosun
The transition from day to night includes several elements.
The scheme above illustrates the transition. First, the sun disappears by moving away. Then, the moon replaces it by moving in.
Other elements animate in the middle of this transition:
- Theme: Changes from the day theme to the night theme with a fade in/fade out transition of its colors.
- TodayDetailsWidget: Moves and fades at the same time.
The night-to-day transition is the same, but you swap the sun and moon while the theme transition goes from the night theme to the day theme. Some animations run one after the other, while others animate in parallel.
This is how the animation will look when you finish this tutorial:
Using Implicit Animations
You’ll start by animating the sun and the moon during the day-to-night transition. The sun moves to the left, and the moon appears from the right.
You have several options to do this using implicitly animated widgets, such as AnimatedSlide
and TweenAnimationBuilder
. Here, you’ll use the latter, which needs a Tween
.
Applying Tween Animations
Tween
defines the starting and ending points of your animations, as its definition suggests:
Tween<T>(begin: T, end: T)
To interpolate between 0.0 and 1.0, for instance, you’d write:
Tween<double>(begin: 0.0, end: 1.0)
It works with many objects, and you can even try it with Colors
!
Tween
interpolates between two values using the operators +, – and * in its lerp
method. This means you can make Tween
between custom objects if you implement these. It also means some objects can’t interpolate well since they don’t do it or their operators don’t fit. For example, the operator * in the int
class returns num
instead of int
, which is the reason why Tween
can’t interpolate it.
There are a few prebuilt Tween
s for these cases. For example, you can use IntTween
for int
or ConstantTween
, which is a Tween
that stays at the same value. See the implementers of Tween
for the full list.
Implementing Curve Animations
Instead of playing your animations linearly, you can apply different curves to them. Curves
includes the most common ones, like easeIn
and easeOut
. See them animated in the docs.
There are several ways to apply a curve to your animations. One is to apply the curve directly to a Tween
by calling chain()
on it:
Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.slowMiddle));
This Tween
would have a slowMiddle
curve.
Animating the Sun and the Moon Implicitly
To see how implicit animations work and what their limitations are, you’ll try animating the sun and moon implicitly first.
Replace the contents of _sunOrMoon()
in lib/ui/home_page.dart with the following:
return Stack(children: [
// TweenAnimationBuilder for the sun
TweenAnimationBuilder<Offset>(
duration: const Duration(seconds: 2),
curve: Curves.bounceIn,
tween: Tween<Offset>(
begin: const Offset(0, 0), end: const Offset(-500, 0)),
child: const SunWidget(),
builder: (context, offset, child) {
return Transform.translate(offset: offset, child: child);
},
),
// TweenAnimationBuilder for the moon
TweenAnimationBuilder<Offset>(
duration: const Duration(seconds: 2),
curve: Curves.bounceOut,
tween:
Tween<Offset>(begin: const Offset(500, 0), end: const Offset(0, 0)),
child: const MoonWidget(),
builder: (context, offset, child) {
return Transform.translate(offset: offset, child: child);
},
)
]);
Here, you use two TweenAnimationBuilder
s to animate SunWidget
and MoonWidget
implicitly.
Your TweenAnimationBuilder
has several arguments:
- duration: Your animation duration.
-
curve: Here, you make
SunWidget
bounceIn
andMoonWidget
bounceOut
. -
tween: You used
Tween
to make the translation animation. -
child: The non-moving part of your widget. In this case, the children are
SunWidget
andMoonWidget
. -
builder: Where you decide how to animate. Here, you used
Transform.translate()
.TweenAnimationBuilder
calls it several times with an updatedoffset
interpolated from yourTween
.
Note that you display both the sun and the moon in a Stack
because you haven’t handled the transition between the themes yet.
Hot reload to see the result.
Though you get an animation, it only animates on startup. It’s not easy to start it on a button press or launch the moon animation after the sun animation finishes. This shows that implicit animations are not the best tool 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.
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.
Animating the TodayDetails Widget
This widget is composed of two parts: The left one displays temperature, and the right one displays wind and weather type. The animation will take part in three phases:
- Move away from the original position and become transparent.
- Stay transparent away for a while.
- Move back to the original position and become visible.
In this case, you’ll need to use TweenSequence
, which allows you to separate a Tween
in several parts. Here’s an example:
TweenSequence<double>([
TweenSequenceItem(tween: Tween<double>(begin: 0.0, end: 0.5), weight: 4),
TweenSequenceItem(tween: ConstantTween<double>(0.5), weight: 2),
TweenSequenceItem(tween: Tween<double>(begin: 0.5, end: 1.0), weight: 4),
]);
Each TweenSequenceItem
has a weight used to determine its duration. For example, here you have a total weight of 10 with the following repartition:
- Go from 0.0 to 0.5 with a weight of 4, which is 4/10 of the total
TweenSequence
time. - Stay at 0.5 with a weight of 2, which is 2/10 of the total
TweenSequence
time. - Go from 0.5 to 1.0 with a weight of 4, which is 4/10 of the total
TweenSequence
time.
Interpolating a Custom Object
You need to animate two properties at once: Offset
for the movement and double
for the opacity. One way of doing it is to make a class that implements the operators used by Tween
for the interpolation. These are *
, +
and -
.
Create a new file, fade_away.dart, in the model directory:
import 'dart:ui';
class FadeAway {
final Offset offset;
final double opacity;
const FadeAway(this.offset, this.opacity);
FadeAway operator *(double multiplier) =>
FadeAway(offset * multiplier, opacity * multiplier);
FadeAway operator +(FadeAway other) =>
FadeAway(offset + other.offset, opacity + other.opacity);
FadeAway operator -(FadeAway other) =>
FadeAway(offset - other.offset, opacity - other.opacity);
}
FadeAway
implements all the mentioned operators to be able to animate it. You simply use Offset
and double
operators.
Go back to home_page.dart, import FadeAway
and add these new variables:
late TweenSequence<FadeAway> _temperatureAnim;
late TweenSequence<FadeAway> _weatherDetailsAnim;
You’ll use TweenSequence
since three phases make up the animation.
Init the TweenSequences
at the end of _initThemeAnims()
:
_temperatureAnim = TweenSequence<FadeAway>([
TweenSequenceItem(
tween: Tween<FadeAway>(
begin: const FadeAway(Offset(0, 0), 1.0),
end: const FadeAway(Offset(-100, 0), 0.0),
).chain(CurveTween(curve: Curves.easeInOut)),
weight: 40,
),
TweenSequenceItem(
tween: ConstantTween<FadeAway>(const FadeAway(Offset(-100, 0), 0.0)),
weight: 20,
),
TweenSequenceItem(
tween: Tween<FadeAway>(
begin: const FadeAway(Offset(-100, 0), 0.0),
end: const FadeAway(Offset(0, 0), 1.0),
).chain(CurveTween(curve: Curves.easeInOut)),
weight: 40,
),
]);
_weatherDetailsAnim = TweenSequence<FadeAway>([
TweenSequenceItem(
tween: Tween<FadeAway>(
begin: const FadeAway(Offset(0, 0), 1.0),
end: const FadeAway(Offset(100, 0), 0.0),
).chain(CurveTween(curve: Curves.easeInOut)),
weight: 40,
),
TweenSequenceItem(
tween: ConstantTween<FadeAway>(const FadeAway(Offset(100, 0), 0.0)),
weight: 20,
),
TweenSequenceItem(
tween: Tween<FadeAway>(
begin: const FadeAway(Offset(100, 0), 0.0),
end: const FadeAway(Offset(0, 0), 1.0),
).chain(CurveTween(curve: Curves.easeInOut)),
weight: 40,
),
]);
Each TweenSequence
has a different target position: Offset(-100, 0)
for _temperatureAnim
and Offset(100, 0)
for _temperatureAnim
. They move toward it, pause with ConstantTween
and finally come back to their origin.
Next, update the call to TodayDetailsWidget()
with:
TodayDetailsWidget(
weatherData: todayWeather,
progress: _animationController,
temperatureTween: _temperatureAnim,
detailsTween: _weatherDetailsAnim,
)
Just like SunWidget
and MoonWidget
, you’ll transform TodayDetailsWidget
into an AnimatedWidget
. Then, you’ll use your TweenSequence
and _animationController
to animate it.
Update TodayDetailsWidget
:
// ...
import '../model/fade_away.dart';
class TodayDetailsWidget extends AnimatedWidget {
final WeatherData weatherData;
final Animatable<FadeAway> temperatureTween;
final Animatable<FadeAway> detailsTween;
const TodayDetailsWidget({Key? key,
required this.weatherData,
required Animation<double> progress,
required this.temperatureTween,
required this.detailsTween})
: super(key: key, listenable: progress);
Animation<double> get _animation => listenable as Animation<double>;
@override
Widget build(BuildContext context) {
// 1
final temperatureCurrentValue = temperatureTween.evaluate(_animation);
final detailsCurrentValue = detailsTween.evaluate(_animation);
final now = DateTime.now();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 2
Transform.translate(
offset: temperatureCurrentValue.offset,
child: Opacity(
child: _temperature(context, now),
opacity: temperatureCurrentValue.opacity,
),
),
const SizedBox(
width: 16,
),
// 3
Transform.translate(
offset: detailsCurrentValue.offset,
child: Opacity(
child: _windAndWeatherText(context, now),
opacity: detailsCurrentValue.opacity,
),
),
],
);
}
TodayDetailsWidget
is a bit different from your previous AnimatedWidget
s, especially in build()
:
- You use
_animation
‘s progress to evaluate eachTween
-interpolatedFadeAway
object instead of directly using the_animation
value in your widgets. - You move and fade the temperature using
temperatureCurrentValue
. - You do the same with
detailsCurrentValue
.
Also, notice that you declare temperatureTween
and detailsTween
as Animatable
instead of TweenSequence
. Both Tween
and TweenSequence
are Animatable
, so you can use any of them without any impact on TodayDetailsWidget
.
Remember that Tweens are not animated — only _animation
changes over time. Here, you get their interpolated value thanks to evaluate()
.
Hot restart and play the animation.
Congratulations! You’ve completed all the planned animations.
Adding More Animations
Use the next exercises to test your new skills.
Add an Animation to BottomCard
Try to animate BottomCard
by letting it rotate, bounce or scale when the theme changes, for instance.
Make a Startup Animation
You might need two AnimationController
for this: one for the day/night transition, one for the startup animation. In this case, don’t forget to change SingleTickerProviderStateMixin
to TickerProviderStateMixin
.
You may add the following in your initState()
:
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
// Do something on startup
});
It will launch at the startup of your widget, which is useful for a startup animation.
Improve CloudyWidget
Try to add some movements to the clouds, like you did for the sun and the moon. Here, consider using .repeat()
instead of forward()
on your AnimationController
. TweenSequence
is also a good fit for this animation.
Make It Rain or Snow
Since you know how to animate widgets, you can also simulate rain and snow. Make rectangles for the rain falling and white circles for the snow using Container
. Then, animate them with Tween
or TweenSequence
that you’ll repeat()
.
Solution
A complete solution is available in the challenges project of this tutorial’s resources.
Build and run, then watch the animations take place. :]
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
This tutorial introduced you to staggered animations. After reviewing their main components, you can now chain many animations or make them overlap each other.
Check out the tutorial Flutter Canvas API: Getting Started to make even more custom animations. It teaches how to draw custom shapes, and even animate them!
You can also achieve great results with implicit animations. Learn how to do a radial menu in Implicit Animations in Flutter: Getting Started.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!