All I Know About AnimationController

I am a freelance mobile app developer, mainly working on Flutter. I'm also developing a Flutter package named "crop_your_image", which enables Flutter app developers to build cropping images functionality in their app with their own designed UI. https://pub.dev/packages/crop_your_image
The basic idea of Flutter's "explicit animation" is so simple: rebuild with updated value.
Imagine if a Container has its size of width: 50, height: 50, it renders a square box with the size of 50. Once we rebuild the widget with passing another value of width: 100, height: 100, the square box is now rendered bigger.

Although this example shows the sudden change in the box's size, what if we gradually make this bigger by rebuilding again and again with a slightly bigger size, like 51, 52, 53… 100, in 1 second? Let's do that.

Whoa, it's an animation! Don't you think so?
Flutter's widget itself doesn't have any ability to "animate" itself. Animation is usually achieved by gradual updates and rebuilds regardless of how they transform or move with animation.
Based on the idea above, in this article, we will focus on how animations, especially “explicit” animations of both "curve-based" and "physics-based" animations, will be done in Flutter.
For broader knowledge about Flutter animations, see the official document below.
https://docs.flutter.dev/ui/animations
AnimationController
So, let's start by taking a look at AnimationController.
As I described, widgets can explicitly animate by rebuilding with gradually updating their value. But how can we achieve "gradually updating"?
I guess some of you may imagine using Timer.periodic(), like below.
double value = 0.0;
late Timer timer;
timer = Timer.periodic(const Duration(milliseconds: 10), (_) {
setState(() => value += 0.1);
if (value >= 1.0) {
timer.cancel();
}
});
...
Opacity(
opacity: value,
child: SomeWidget(),
)
DartPad: https://dartpad.dev/?id=e6485e5418a114ea67f04720a39b3671
Yeah, this actually causes rebuilds and updates the opacity value, that achieves animation.

However, this strategy has several pitfalls. For example:
it doesn't "sync" with Flutter's frame cycle
we can't "reverse", "pause", or other operations about the animation
we have to maintain
valueby ourselvesetc.
Because Timer is not designed to achieve animations, we have many issues to be solved by ourselves when using this for animations.
Here AnimationController comes in.
https://api.flutter.dev/flutter/animation/AnimationController-class.html
As AnimationController has a lot of functionalities for animation, let's start with the default feature that "linearly produces values that range from 0.0 to 1.0" written in the doc above.
late final AnimationController controller;
/// prepare for [AnimationController]
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..addListener(() {
// just print the current value
debugPrint(controller.value.toString());
});
// start animation
controller.forward();
DartPad: https://dartpad.dev/?id=6b825290eb85c5a01f8ec85d460c65fe
By adding a callback that prints the current controller.value by using addListener(), AnimationController calls the callback repeatedly and your debug console will show the logs like below:
flutter: 0.0
flutter: 0.012145
flutter: 0.020478
flutter: 0.028811
...
flutter: 0.987138
flutter: 0.995471
flutter: 1.0
Again, the AnimationController's behavior is so simple by default: exposes gradually changing values from 0.0 to 1.0.
But it’s just a double value from 0.0 to 1.0, and the value is only relevant to limited widgets like Opacity. So how can I animate colors or positions, then?
Now, Tween comes next.
Tween
Tween calculates the value "between two values”, such as 1.0 and 15.0, Colors.red and Colors.blue, or Offset(0, 10) and Offset(10, 20).
AnimationController can cooperate with Tween by calling drive() method, and it enables us to obtain gradually changing values of whatever type and whatever range.
late final Animation<double> _sizeAnimation;
late final Animation<Color?> _colorAnimation;
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..addListener(() {
setState(() {
// obtain current size value of [Tween]
_size = _sizeAnimation.value;
// obtain current color value of [ColorTween]
_color = _colorAnimation.value;
});
});
// Tween for [double] value
_sizeAnimation = _controller.drive(Tween(begin: 50.0, end: 150.0));
// Tween for [Color] value
_colorAnimation = _controller.drive(
ColorTween(begin: Colors.blue, end: Colors.red),
);
DartPad: https://dartpad.dev/?id=cff161cd5a6499e02e15cb8e33f88757

So far, we have discussed how we can explicitly animate our widgets. Though those animations can be achieved by using "implicit" animations as well, such as AnimatedContainer or AnimatedOpacity, this basics of explicit approach is important to understand how we implement "physics-based" animations.
Physics
"Physics" sounds difficult, difficult with complex formulas and calculations. However, as long as we understand the basics of explicit animations discussed so far, the Flutter framework will nicely hide those complexities for us.
So, let's take a closer look at "physics-based" animations.
AnimationController again
To achieve physics-based animations, we still use AnimationController with another method animateWith() and additional object Simulation.
_controller.animateWith(
GravitySimulation(2000, 20, 1000, 0),
);
DartPad: https://dartpad.dev/?id=a583cf912190af1632b34c0806f90399

Simulation is an abstract class and we have three sub-classes for physics: GravitySimulation, FrictionSimulation, and SpringSimulation.
Curve v.s. physics
Before starting the discussion of how we use those Simulation classes, we have to clarify the difference between "curve-based" animation and "physics-based" animation.
While the default AnimationController updates its value linearly from 0.0 to 1.0, or between configured range, Curve will change the behavior of how the value is updated over time.
For instance, Curves.easeInOut updates the value with a cubic animation curve, meaning it "starts slowly, speeds up, and then ends", as described in the document below.
https://api.flutter.dev/flutter/animation/Curves/easeInOut-constant.html
Flutter provides a lot of built-in Curves, found in the list below, and they usually satisfy us.
https://api.flutter.dev/flutter/animation/Curves-class.html
On the other hand, however, when considering the situation that an animation is "interrupted" and another animation follows, curve animations look slightly "not natural", with sudden stop and sudden start, because curve animations just simulate the animation without considering “how it starts”.
Here, physic-based animation will solve the issue.
The biggest difference between curve-based and physics-based is whether it accepts velocity or not. (of course the behavior of how the value changes is also different, though)
velocity is a value represented by double, which describes "magnitude".
For example, 1000 represents the strong magnitude, or 10 represents the weak one, and so on.
By passing the initial velocity when starting animations, animations don't start suddenly and look more “natural”.

DartPad: https://dartpad.dev/?id=7c4a910b5862909870a5d1148cfca3e5
While curve animations can be easily available, with just choosing one from Curves, physical animation will make our animation smoother and more expressive.
Based on this point, let's move on to three concrete physical simulations.
GravitySimulation
GravitySimulation simulates, gravity!
In the real world, once we "drop" something, such as a dropped apple from a tree, it starts animation(falling) slowly and then gradually accelerates its speed. GravitySimulation simulates this behavior.
As you imagine, we can achieve "dropping" animation by applying gradually changing Offset to whatever widget using this GravitySimulation and AnimationController.

late final AnimationController _controller
/// prepare for the [AnimationController]
_controller = AnimationController.unbounded(vsync: this)..addListener(() {
setState(() => _value = _controller.value);
});
/// apply _value to the [top] argument
Stack(
children: [
CustomPaint(size: Size.infinite, painter: _GridPainter()),
Positioned(
top: _valueY,
left: 20,
child: Container(width: size, height: size, color: Colors.blue),
),
Positioned(
top: _valueY + 100,
left: 100,
child: Text('Hello Physics', style: TextStyle(fontSize: 20)),
),
Positioned(
top: _valueY + 200,
left: 60,
child: Icon(Icons.arrow_downward, size: 40),
),
],
),
// start animation with [animatedWith] and [GravitySimulation]
_controller.animateWith(GravitySimulation(2000, 20, 1000, 0));
DartPad: https://dartpad.dev/?id=f092258d3a8e603c724a2eb979b20d43
Note that we usually have to instantiate AnimationController by calling AnimationController.unbounded() to implement physics-based animations.
It is because AnimationController just simulate the value from 0.0 to 1.0 by default, but we usually want to customize the from/to value for physical simulations, or sometimes we don't even want to specify the to value as we don't know when the physical simulation will stop.
That’s why .unbounded() is suitable for most physical simulations.
Looking at GravitySimulation, it receives 4 parameters as a configuration for its animation:
acceleration: how much it accelerates, in other words, gravity.distance: initial value that is exposed byAnimationController.endDistance: the value whereAnimationControllerstops the animation.velocity: initial velocity applied when the animation starts.
Here, I have to apologize for one thing that I don’t know much about physics and how the parameters can be calculated. So let me just focus on how to use the API applying "approximately relevant” values (2000 for acceleration, for example).
Anyways, you feel so simple, don't you? All the difference from the usage of the very first AnimationController is the method to be called and its argument. We can just start the animation by calling animateWith() here and just apply the exposed value to widgets, that's it!
Another interesting fact is that AnimationController also exposes current velocity. This enables us to pass it to the next animation, which makes the animation more natural.
One example of combining animations is "bouncing".
Though GravitySimulation doesn't simulate bouncing by default, we can easily achieve this behavior by using velocity with the strategy below:
start "falling" animation
once the value comes to the value represented as "ground", stop animation.
obtain exposed
velocityfromAnimationControllerand customize the direction into "negative".start another "falling" animation with the initial velocity created at the previous step.
_controller = AnimationController.unbounded(vsync: this)..addListener(() {
setState(() {
_valueY = _controller.value;
});
// y of the ground
final ground = MediaQuery.of(context).size.height - 100 - boxSize;
if (_value > ground) {
// box now touches the ground
// retrieve the velocity of slightly weaker to opposite(upper) direction
final velocity = _controller.velocity * -1 * 0.7;
// stop previous animation
_controller.stop();
// start new animation
_controller.animateWith(
GravitySimulation(2000, ground, 1000, velocity),
);
}
});
DartPad: https://dartpad.dev/?id=7746a8b6bca0b6da122cd60e4943df3e

Fantastic! We can now introduce a "bouncing block" to our app without using any packages or animation tools. You can try this on DartPad linked above.
You may notice that one GravitySimulation only exposes one double value, which means we can't simulate the animation with Offset or other types. But it's ok, let's take a look at other Simulations first and get back to how we can combine them into 2D animations.
FrictionSimulation
While GravitySimulation gradually accelerates its speed, FrictionSimulation gradually decelerates the speed depending on the parameter drag.
Because FrictionSimulation starts with a given velocity, and then, it gets weaker and weaker, slower in other words, initial velocity is also required like below.
FrictionSimulation(
0.5, // drag
initialPosition,
1000.0, // initial velocity. 0 means no animation happening
);
To use FrictionSimulation, we typically configure these three values like below:
drag: how fast the animation slows down. Higher means more friction (stops faster), and typically between0.001and0.5.position: the initial value exposed byAnimationController.velocity: the initial velocity for friction calculation.
One interesting fact that is good to know is that GestureDetector.onPanEnd also exposes velocity with the type of Offset, which describes how fast and in what direction users panned.
Thus, by combining the velocity and FrictionSimulation, we can easily achieve the animation of sliding objects.

/// [AnimationController] for x axis
_controllerX = AnimationController.unbounded(vsync: this)..addListener(() {
setState(() {
_valueX = _controllerX.value;
});
});
/// [AnimationController] for y axis
_controllerY = AnimationController.unbounded(vsync: this)..addListener(() {
setState(() {
_valueY = _controllerY.value;
});
});
void _startFriction(Offset initialPosition, Offset velocity) {
// start animation for x axis
_controllerX.animateWith(
FrictionSimulation(0.5, initialPosition.dx, velocity.dx),
);
// start another animation for y axis
_controllerY.animateWith(
FrictionSimulation(0.5, initialPosition.dy, velocity.dy),
);
}
/// GestureDetector to grab and slide the box
GestureDetector(
// grab and move the box
onPanUpdate: (details) {
setState(() {
_valueX = details.globalPosition.dx - size / 2;
_valueY = details.globalPosition.dy - size / 2;
});
},
// slide the ball
onPanEnd: (details) {
_startFriction(
Offset(_valueX, _valueY),
details.velocity.pixelsPerSecond, // velocity exposed by GestureDetector
);
},
child: Stack(
children: [
Positioned(
top: _valueY,
left: _valueX,
child: Container(width: size, height: size, color: Colors.blue),
),
],
),
),
DartPad: https://dartpad.dev/?id=61c4779abc68441c828cf5250fc4b431
Also, by combining the previous GravitySimulation for the y-axis value, we can implement a "throwing" feature like below.

void _startFriction(Offset initialPosition, Offset velocity) {
_controller.animateWith(
FrictionSimulation(0.5, initialPosition.dx, velocity.dx),
);
/// simulate y-axis with [GravitySimulation] with previous bouncing logic
final ground = MediaQuery.of(context).size.height - 100 - size;
_controllerY.animateWith(
GravitySimulation(2000, initialPosition.dy, ground, velocity.dy),
);
}
DartPad: https://dartpad.dev/?id=f6bf1b563d4f4339cb14e98123914da4
Still the basic rule of using AnimationController is the same, even when we want to simulate 2D animations: make AnimationControllers for each value of the x-axis and y-axis and apply each value separately.
SpringSimulation
SpringSimulation may be the closest to the curve-based animation the most in terms of its usage.
Both require the initial/destination value and the configuration to calculate how the value would be updated over time (Curve for curve-based animation).
One of the biggest differences is that SpringSimulation doesn't accept Duration because the duration of the animation is decided by the result of the simulation, meaning it can't be fixed.
Other than that, we can easily understand how to use SpringSimulation by looking at the sample below.
late AnimationController controllerX;
/// [AnimationController] for x axis
_controllerX = AnimationController.unbounded(vsync: this)..addListener(() {
setState(() => _valueX = _controllerX.value);
});
/// [AnimationController] for y axis
_controllerY = AnimationController.unbounded(vsync: this)..addListener(() {
setState(() => _valueY = _controllerY.value);
});
/// configuration values for [SpringSimulation]
static const description = SpringDescription(
mass: 0.8,
stiffness: 50,
damping: 10,
);
void _startAnimation(Offset from, Offset to, Offset initialVelocity) {
// make [SpringSimulation] for x-axis
final simulation = SpringSimulation(
description,
from.dx,
to.dx,
initialVelocity.dx,
);
// start x-axis animation
_controllerX.animateWith(simulation);
// make [SpringSimulation] for y-axis
final simulation = SpringSimulation(
description,
from.dx,
to.dx,
initialVelocity.dx,
);
// start animation for y-axis
_controllerX.animateWith(simulation);
)
DartPad: https://dartpad.dev/?id=4e7258fae87c2d858b2a8b06163d343e

As we can see, the values for the configuration are not specified directly. Instead, we use SpringDescription with passing mass, stiffness, and damping.
Again, because I'm not bright about what each value describes and how we can decide each value, I just introduce springster package for nicely pre-defined SpringDescription as well as widgets to achieve physic-based animations more easily.
https://pub.dev/packages/springster
Also, the Flutter framework will introduce a handy factory for SpringDescription in the near future, according to the pull request below.
https://github.com/flutter/flutter/pull/164411
By using SpringDescription, not only our animations look smoother than curve-based animations, but also velocity will naturally transition to the next animation when interrupted.
Wrap up
So far, we've seen Flutter's "explicit" animations that can be implemented with AnimationController.
We are now clear that both "curve-based" and "physics-based" animations are done in the similar manner: AnimationController exposes the updating value based on the given configuration, and we can just apply our widgets with rebuilds.
I believe some of you may be surprised that "physics-based" animations can be implemented much easier with fewer lines of code than expected without packages.
Now, even if you don't find any package fit to your use-case, this article will hopefully give you an idea to achieve the goal by yourself.
Bonus Information
For curve-based animations and spring simulations, my package named animated_to will let you animate whatever widgets in the same manner with Flutter’s implicit animations, such as AnimatedContainer or AnimatedOpacity.
https://pub.dev/packages/animated_to
With animated_to package, what you have to do is just wrap your widget with AnimatedTo.curve or AnimatedTo.spring and update its position by rebuilds.
Stack(
children: [
Positioned(
left: _isLeft ? 20 : null,
right: _isLeft ? null : 20,
top: 20,
child: AnimatedTo.spring(
globalKey: _key,
child: MyWidget(),
),
),
],
)

AnimatedTo calculates the from and to position in both the x-axis and y-axis and also maintainsvelocity when animations are interrupted.
This is simple but so powerful! If you are interested in animated_to, feel free to visit pub.dev and try your animations!



