Flutter Animations Simplified I
It is well known that Flutter offers a very simple way for building beautiful cross-platform applications (Android, iOS, Desktop and Web). From the perspective of an Android Developer (like me), this can be true at many levels, since you can create your UI with declarative and functional code, using easy concepts such as Column or Row, instead of diving into XML files (which are not code), or even storyboards (if you come from iOS).
If you’ve worked with Android animation APIs, you’ll know that they are a headache at many levels: a complex interface, a lot of APIs (some of them deprecated), and, at the end, you end up not knowing how to implement your animations (I use MotionLayout, by the way).
This is not the case for Flutter animations, since there is just a way to implement them!
This series of articles pretend to explain Flutter animations in a concise and simplified manner. One of the main goals is to get you to write your animations almost immediately, and dive into details later. This approach let us to experiment and play with our code and start asking questions by ourselves.
IMPORTANT: all the code present in this article is compatible with Flutter 2!
Implicit Animations
Implicit Animations are no more than widgets that manage all the animation hard part for you. Actually, these widgets are very similar to those that you might’ve already implemented in your code. I’m talking about the AnimatedPadding, animated counterpart of Padding widget; AnimatedOpacity, animated counterpart of Opacity widget, and so on. These two examples are very punctual, and their behavior corresponds a lot to their name (that IS good class naming!).
These widgets needs just a few things to work: a child (that can be any widget), a duration (given in hours, minutes, seconds, milliseconds, etc.), and a value that must be managed by a StatefulWidget. This last point is very important, since every time the value is changed inside the setState function (hence, the widget is rebuilt), the animation will automatically be launched by using an interpolation of the old and the new value.
class AnimatedDitto extends StatefulWidget {
@override
_AnimatedDittoState createState() => _AnimatedDittoState();
}class _AnimatedDittoState extends State<AnimatedDitto> {
double opacity = 0.0;@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
child: Image.asset( // <--- The child
'assets/ditto.png',
fit: BoxFit.fill,
),
duration: Duration(seconds: 3), // <-- The duration
opacity: opacity, // <-- The value
),
TextButton(
onPressed: _changeOpacity,
child: Text('Animate'),
)
],
);
} void _changeOpacity() {
setState(() {
opacity = 1.0;
});
}
}
The previous snippet changed the value of the variable opacity inside a setState function, which was translated in an awesome and easy fade in transition for our Ditto. And, at the end, this widget handled all the animation stuff by itself. The only thing we needed to do was to change the state of our widget.
AnimatedContainer widget
This is great, you can only use this widgets to start using animations in your app! But there is one problem: the animations seen so far only animate one property of our child. That is not very flexible, and may cause a lot of scalability problems if we just wrap widgets without control, obviously leading to a poor and cryptic code (you know that Dart can be an indentation hell if not written correctly).
The panacea: AnimatedContainer.
Its behavior is exactly the same as a common Container, but the best part of it is that any of its properties can be animated! The only thing we need to do is to decide what changes are going to be made to our child.
Let’s say that we want to change both height and width of our container, and its color too (yeah, colors can be interpolated). The first thing we’d need to do is to define the fields of our StatefulWidget:
class _AnimatedDittoState extends State<AnimatedDitto> {
var dittoHeight = 100.0;
var dittoWidth = 100.0;
var backgroundColor = Colors.white;
Use them in our AnimatedContainer:
AnimatedContainer(
height: dittoHeight,
width: dittoWidth,
color: backgroundColor,
child: Image.asset('assets/ditto.png', fit: BoxFit.fill),
duration: Duration(seconds: 3),
)
And decide what are going to be the new values that’ll be assigned in our setState function:
void _changeValues() {
setState(() {
// new values to interpolate
dittoWidth = 300;
dittoHeight = 300;
backgroundColor = Colors.deepPurpleAccent;
});
}
As you can see, more than one property is being animated at the same time by using AnimatedContainer.
I said that any property can be animated in this widget. Let’s give a try and animate its decoration parameter following the previous steps:
class _AnimatedDittoState extends State<AnimatedDitto> {
// the two new properties used in a BoxDecoration
var borderRadius = 0.0;
var backgroundColor = Colors.yellow;
Then, we use them in our container (in this case, inside BoxDecoration):
AnimatedContainer(
height: dittoHeight,
width: dittoWidth,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
),
duration: Duration(seconds: 3),
child: Image.asset('assets/ditto.png', fit: BoxFit.fill),
)
Finally, we reassign their values in a setState block:
void _changeValues() {
setState(() {
// reassign values
backgroundColor = Colors.deepPurple;
borderRadius = 150.0; dittoWidth = 300;
dittoHeight = 300;
});
}
As I said, ANY property of Container widget can be animated. :)
Finally, we can even create a little sequential choreography for our little friend Ditto. But this would imply to track the state of the class fields, by adding a lot of new variables, a lot of conditionals to set the new values, and doing so would lead us to a lot of boilerplate.
The idea is to get something like this:
Hint: create a new data class to hold all the values you want to be animated!
Giving life to your animations: Duration and Curves
The concept of the Duration class is kind of self explanatory. You have a good level of granularity that goes from days (it seems hard to imagine an animation like that) to microseconds. But the most part of the time you’ll be using milliseconds to coordinate your animations.
You can even make a combination. This Duration:
Duration(milliseconds: 1500)
Is equivalent to:
Duration(seconds: 1, milliseconds: 500)
But internally, this class will handle everything in milliseconds.
By default, the interpolation happens linearly. It describes a simple straight line in the cartesian plane. This line can be described by: f(x) = mx + b
However, Flutter has a mechanism called Curves that will make our animation a little bit more realistic. With this, our interpolation will be still in the range of the previous and new value of our stateful widget, but the way between them will be different, and it will describe different curves in the cartesian plane.
For example, instead of using a simple straight line, we can use a parabola (this is: f(x) = ax² + bx + c) to create an acceleration effect. This is done by using the easeIn curve that, according to the documentation, it is:
A cubic animation curve that starts slowly and ends quickly
AnimatedContainer(
curve: Curves.easeIn,
height: dittoHeight,
width: dittoWidth,
color: Colors.purple,
child: Image.asset('assets/ditto.png', fit: BoxFit.fill),
duration: Duration(milliseconds: 1000),
)
Flutter has many built-in curves that you can see and choose whatever you like the most.
Finally, you can implement you own curve by extending the Curve abstract class and overriding its transformInternal method for whatever curve we like:
class SineCurve extends Curve {
final double frequency;SineCurve({this.frequency = 1});@override
double transformInternal(double t) {
return sin(2 * pi * frequency * t);
}
}
The Repo
You can check all of the examples in my Github repo.
Conclusion
This is just away to get started with animations. I you just want your app to look great without doing a really big effort. The interface is simple and easy to use. But there are more resources that allow us to have more control in our animations. But that is a topic for the next one. :)