Animations are a great way to spice up your Flutter apps and delight your users.
So let's learn about them! We'll cover:
- Implicitly Animated Widgets
- Tweens & TweenAnimationBuilder
- AnimationController & AnimatedBuilder
- Built-in Explicit Transition Widgets
These are just the basics. So I'm also sharing the source code for a new gallery app. This contains many examples of animations that you can learn from and use in your projects:
Ready? Let's get started!
Implicitly Animated Widgets
Flutter ships with a bunch of so-called implicitly animated widgets that you just drop in your code to easily add animations.
For example, let's take a look at AnimatedContainer.
AnimatedContainer
Here's a very boring Container
with a given width
, height
, and color
.
Container(
width: 200,
height: 200,
color: Colors.red,
)
Let's replace it with AnimatedContainer
and give it a duration
:
AnimatedContainer(
width: 200,
height: 200,
color: Colors.red,
duration: Duration(milliseconds: 250),
)
To animate this, we need to do a few things:
// 1. Convert the parent class to a StatefulWidget
class AnimatedContainerPage extends StatefulWidget {
@override
_AnimatedContainerPageState createState() => _AnimatedContainerPageState();
}
class _AnimatedContainerPageState extends State<AnimatedContainerPage> {
// 2. declare the container properties as state variables
double _width = 200;
double _height = 200;
Color _color = Colors.red;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedContainer(
// 3. pass the state variables as arguments
width: _width,
height: _height,
color: _color,
duration: Duration(milliseconds: 250),
),
),
// 4. add a button with a callback
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: _update,
),
);
}
// 5. Update the state variables to rebuild the widget
void _update() {
setState(() {
_width = 300;
_height = 300;
_color = Colors.green;
});
}
}
With the changes above, we can press the button and the container animates to the new values over the given duration:
But if we press the button again, nothing happens because the state variables are already set to the updated values.
To make things more interesting, we can modify the _update
method to use a random number generator, so that our container animates to a different set of values every time we press the button.
final random = Random();
void _update() {
setState(() {
_width = random.nextInt(300).toDouble();
_height = random.nextInt(300).toDouble();
_color = Color.fromRGBO(
random.nextInt(128),
random.nextInt(128),
random.nextInt(128),
1,
);
});
}
Not good enough? Then we can change the animations curve to modify the rate of change for the animation value:
AnimatedContainer(
width: _width,
height: _height,
color: _color,
duration: Duration(milliseconds: 250),
// default curve is Curves.linear
curve: Curves.easeInOutCubic,
)
This way the animation feels a lot more natural.
Flutter comes with a wide range of curves that you can choose from. And if none of the built-in curves work for you, you can even define your own
Curve
subclasses.
// a custom sine curve that can be passed to any of the implicitly animated widgets
class SineCurve extends Curve {
final double count;
SineCurve({this.count = 1});
@override
double transformInternal(double t) {
return sin(count * 2 * pi * t) * 0.5 + 0.5;
}
}
Alongside AnimatedContainer, Flutter ships with many other implicitly animated widgets such as AnimatedAlign, AnimatedOpacity, AnimatedTheme, and many more. Here is the full list.
How do implicitly animated widgets work?
Implicitly animated widgets have one or more animatable properties that can be set to a target value. When the target value changes, the widget animates the property from the old value to the new value over the given duration.
This makes them easy to use, because all you have to do is to update the target value(s), and the widget will take care of the animation(s) under the hood.
However, they can only be used for animations that go forward. If you need an animation that repeats or goes in reverse, you'll need an explicit animation (see below).
Tweens & TweenAnimationBuilder
Tween stands for in-between, and represents a range with a beginning and an end:
We can use tweens to represent animation values within that range.
And Flutter gives us a TweenAnimationBuilder that we can use to define our own custom implicit animations.
Use
TweenAnimationBuilder
if you need to create a basic animation, but none of the built-in implicit animations widgets (e.g. AnimatedFoo) does what you need.
How does this work?
Well, let's get back to the boring Container
:
Container(width: 120, height: 120, color: Colors.red)
We can wrap this with a TweenAnimationBuilder
and give it a Duration
and a Tween
:
TweenAnimationBuilder<double>(
// 1. add a Duration
duration: Duration(milliseconds: 500),
// 2. add a Tween
tween: Tween(begin: 0.0, end: 1.0),
// 3. add a child (optional)
child: Container(width: 120, height: 120, color: Colors.red),
// 4. add the buiilder
builder: (context, value, child) {
// 5. apply some transform to the given child
return Transform.translate(
offset: Offset(value * 200 - 100, 0),
child: child,
);
},
)
The
child
argument is optional and can be used for optimization purposes. For more info, see: Why do TweenAnimationBuilder and AnimatedBuilder have a child argument?
The builder
above gives us an animation value within the range specified by the input Tween
.
In this example, we use it to translate the child widget by value * 200 - 100
on the X-axis. This will map animation values between (0, 1)
to an offset between (-100, 100)
.
If we put the code above inside a new widget class and hot reload, we can see the animation:
At this stage, we can extract the Tween's end value to a state variable and use a Slider
to update it:
// 1. use a state variable
double _value = 0.0;
// 2. pass it to the Tween's end value
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: _value),
...
)
// 3. Add a slider to update the value
Slider.adaptive(
value: _value,
onChanged: (value) => setState(() => _value = value),
)
This way, TweenAnimationBuilder
will automatically animate to the new value when we interact with the Slider
:
Other types of Tweens
In the example above we've used a Tween
of type double
.
But there are several built-in Tween subclasses such as ColorTween, SizeTween, and FractionalOffsetTween that you can use to animate between different colors, sizes, and much more.
If you want, you can even define your own
Tween
subclasses. This is useful if you want to animate between custom objects in your app. See the Tween class documentation for more info on this.
AnimationController
If we want to create explicit animations that can go forward, in reverse, or even repeat forever, we need an AnimationController
.
Let's see how to use it:
// 1. Define a StatefulWidget subclass
class RotationTransitionPage extends StatefulWidget {
const RotationTransitionPage({Key? key}) : super(key: key);
@override
_RotationTransitionPageState createState() => _RotationTransitionPageState();
}
class _RotationTransitionPageState extends State<RotationTransitionPage>
// 2. add SingleTickerProviderStateMixin
with SingleTickerProviderStateMixin {
// 3. create the AnimationController
late final _animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
@override
void dispose() {
// 4. dispose the AnimationController when no longer needed
_animationController.dispose();
super.dispose();
}
}
The most interesting line is this:
late final _animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
By passing
this
to thevsync
argument, we're asking Flutter to produce a new animation value in sync with the screen refresh rate of our device (normally at 60 frames per second). For more info on this, see: Why Flutter animations need a vsync/TickerProvider.
If you have a lot of widgets with explicit animations, setting up an
AnimationController
every time is quite tedious. This article offers two solutions: How to reduce AnimationController boilerplate code: Flutter Hooks vs extending the State class.
AnimatedBuilder
Now that we have our AnimationController
, let's use it to show a rotating Container
.
One way to do this is to use an AnimatedBuilder
inside the build()
method:
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
// 1. use an AnimatedBuilder
child: AnimatedBuilder(
// 2. pass our AnimationController as the animation argument
animation: _animationController,
// 3. pass the child widget that we will animate
child: Container(width: 180, height: 180, color: Colors.red),
// 4. add a builder argument (this will be called when the animation value changes)
builder: (context, child) {
// 5. use a Transform widget to apply a rotation
return Transform.rotate(
// 6. the angle is a function of the AnimationController's value
angle: 0.5 * pi * _animationController.value,
child: child,
);
},
),
),
);
}
However if we run this code, nothing happens. And that's because we forgot to start the animation!
So we can override the initState()
method and call forward()
:
@override
void initState() {
super.initState();
_animationController.forward();
}
This will "play" the animation once when we load the page (or hot-restart).
If we prefer, we can make the animation repeat forever by calling _animationController.repeat()
instead:
How does AnimatedBuilder work?
Let's revisit this again:
AnimatedBuilder(
// pass our AnimationController as the animation argument
animation: _animationController,
// pass the child widget that we will animate
child: Container(width: 180, height: 180, color: Colors.red),
// add a builder argument
builder: (context, child) {
// use a Transform widget to apply a rotation
return Transform.rotate(
// the angle is a function of the AnimationController's value
angle: 0.5 * pi * _animationController.value,
child: child,
);
},
)
AnimatedBuilder
takes an argument of type Animation<double>
. Since AnimationController
extends Animation<double>
, we can pass it as an argument.
This will cause the builder
to be called every time the animation value changes. We can use this to transform the given child widget, or even return a completely new widget that depends on the animation value.
Built-in explicit transition widgets
AnimationController
& AnimationBuilder
are very powerful and you can combine them to create some very custom effects.
But sometimes you don't even need an AnimatedBuilder
because Flutter already comes with a set of built-in transition widgets that you can use.
For example, we can replace all the code above with a RotationTransition
and accomplish the same thing with much less effort:
RotationTransition(
turns: _animationController,
child: Container(width: 180, height: 180, color: Colors.red),
)
Don't like rotations? How about a scale transition?
ScaleTransition(
scale: _animationController,
child: Container(width: 180, height: 180, color: Colors.red),
)
With this change we get a scale animation that repeats indefinitely:
AnimationController Listeners
AnimationController
can do a lot more than what we've seen so far. For example, we can add a status listener that makes the animation alternate forward and in reverse every time it completes:
@override
void initState() {
super.initState();
// add a status listener
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
_animationController.forward();
}
});
// start the animation when the widget is first loaded
_animationController.forward();
}
CurvedAnimation
Another cool thing we can do is to use a Tween
to generate a new Animation
object from the parent AnimationController
. This is often used to add a custom animation curve:
late final _customAnimation = Tween(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
Once we have this, we can pass it to our transition widget:
ScaleTransition(
// use _customAnimation rather than _animationController as an argument
scale: _customAnimation,
child: Container(width: 180, height: 180, color: Colors.red),
)
And by combining the status listener code above with this CurvedAnimation
, we get this:
Implicit vs Explicit animation widgets
Now that we've covered all the basics, we should spot some common patterns in how the Flutter animation APIs are named and used:
Implicitly animated widgets
- they are named
AnimatedFoo
(AnimatedContainer
,AnimatedAlign
etc.) - they take
Duration
andCurve
arguments - they can only animate forward
- can't find a built-in implicitly animated widget for what you need? Use
TweenAnimationBuilder
.
Explicit animation widgets
- they are named
FooTransition
(RotationTransition
,ScaleTransition
etc.) - they take an
Animation
argument - they can animate forward, in reverse, or repeat forever
- can't find an explicit animation widget for what you need? Use
AnimatedBuilder
.
What other patterns do these APIs have in common?
- all implicitly animated widgets extend the same
ImplicitlyAnimatedWidget
parent class. - all explicit transition widgets extend the same
AnimatedWidget
parent class.
In fact, sometimes it's more convenient to define an AnimatedWidget
subclass rather than using AnimatedBuilder
.
For example, here is how the RotationTransition
widget is implemented inside the Flutter SDK:
class RotationTransition extends AnimatedWidget {
const RotationTransition({
Key? key,
required Animation<double> turns,
this.alignment = Alignment.center,
this.child,
}) : super(key: key, listenable: turns);
// the parent listenable property has type Listenable,
// so we use a getter variable to cast it back to Animation<double>
Animation<double> get turns => listenable as Animation<double>;
final Alignment alignment;
final Widget? child;
// This build method is called every time the listenable (animation) value changes.
// As such, AnimationBuilder is not needed.
@override
Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform = Matrix4.rotationZ(turnsValue * math.pi * 2.0);
return Transform(
transform: transform,
alignment: alignment,
child: child,
);
}
}
In fact, checking the source code for the built-in animation widgets is a great way to learn how to make your own!
Flutter Animations Gallery
There is a lot more to animations than what we have covered. For example:
- how to create staggered animations?
- how to use a
GestureDetector
to drive the animation of completely custom UI widgets? - how to do animated theming in Flutter?
To learn more, check out my Flutter Animations Gallery on GitHub, which is a showcase all the most common animation APIs:
Official documentation
The Flutter docs have some extensive documentation, codelabs, and tutorials about the animation APIs. Here is the best place to get started:
Flutter Animations Course
If you're serious about animations and want to learn how to use them in a real-world app, check out my Flutter Animations Masterclass.
This will teach you how to build a habit-tracking application with completely custom UI and animations. And it includes 7 hours of in-depth content, full source code, extra challenges & much more.