Deep dive into Flutter Curves
I will explain you in details how we can (and cannot) use Curves in Flutter, through a little application I made. But before dive into Curves, let refresh our mind with the main components involved in animations.
Very short reminder
In Flutter, we create our custom animation using 3 kind of objects: Animatables, Animation and AnimationController. For each kind, here is a short description, focusing only on details related to curves behavior.
- Animatable : an animatable is an object that produce a value, given an input generally from 0.0 to 1.0. It will be done thanks to the method transform. Tween and CurveTween are Animatable (we will have a look at these class later).
- Animation : an animation encapsulate an animatable object and a “parent” animation. With these two attributes, the animation can produce values by using the transform method of its animatable with the value of the parent. Notice that the AnimationController is an Animation itself. As developer, we don’t retain animatable directly; we prefer keep instances of Animation instead.
- AnimationController : the main object that will control the animation. During the instantiation, it creates a Ticker, object that will trigger a callback whenever a frame triggers. In other terms, when the framework schedule a new frame, the animation controller’s callback will be executed. In that callback, every objects listening to it will be notified. Consequently, a widget can listen to an AnimationController in order to know when to refresh.
I introduced two example of commonly used Animatable: Tween and CurveTween. A tween is an object that will interpolate (linear interpolation) between two values. By default Tween are used to interpolate ‘num’ instances, and added to that some Tween subclasses have been created in Flutter framework, to interpolate Size, Rect, or Color for example.
In that article , we will focus only on CurveTween. This tween will interpolate a value from 0.0 to 1.0 given a object of type Curve. A Curve is a programming representation of a mathematical function (one dimension), applied on interval [0.0, 1.0].
This is not so obvious to understand the difference between Curve, CurveTween and Animation; it’s much more related to responsibility:
- Curve provide data through a mathematical function.
- Tween (in general) provide the interpolation between two values. In our case, CurveTween is more a wrapper checking inputs and delegating values generation to its curve. Added to that, Tweens have other feature such as the ability to chain to another Animatable, resulting to a new Animatable object (I will explain it later).
- Animation retrieve value from a parent Animation (such as the AnimationController), and delegate the value transformation to its Animatable. We commonly use that type of object to hold an animation’s attribute. More features are available, such as listeners and animation status listener.
It was a short presentation of the main components involved in a Flutter animations. To be honest, we could say many more things about it, but this article focus on Curves.
Curve characteristics
Well, as I said previously, a Curve is a one dimension representation mathematical function existing in a discrete interval: [0.0, 1.0]. It means that given an input value from 0.0 to 1.0 (refered as “t” input value in the code) , the curve will transform it (apply its mathematical function), to produce a result. The result value is generally in interval [0.0, 1.0], with some exceptions for elastic curves and curves named with suffix ‘Back’ (like easeInBack for example).
“A [Curve] must map t=0.0 to 0.0 and t=1.0 to 1.0.” — Flutter framework
Other characteristic: a curve can be flipped. Below an explanation of the developer of the Flutter framework:
“Returns a new curve that is the reversed inversion of this one” — Flutter framework
I know this sentence is a little bit complicated to understand, but I made a little scheme to let you visually understand what the developer meant. I applied transformations described by the Flutter developer to an image of the curve Curves.easeInQuint, so please focus only on the blue curve.
Let’s see how it works in practice!
“WTF my curve viewer flip button is not working only for the linear curve” — Me, just before I noticed that the linear curve and its flipped version are identical…
At this moment, I knew I needed fresh air!
Chaining CurveTweens
I previously introduced the notion of chaining Animatable objects. Don’t get confused, the term does not mean that you will create a sequence of curves. It’s more about which Animatable will lead the transformation of the value, where a child Animatable chains a parent Animatable (child.chain(parent)). The parent will be executed first to get a value, then this value will be the input one for the child.
For example, let’s take f(x) and g(x) our two mathematical functions wrapped into two CurveTween. Chaining f(x) to g(x) will produce a new mathematical function h(x) equal to f(g(x)). Let’s say f(x) = 2x and g(x) = 1/(x+1). In that case, h(x) = 2*(1/(x+1)). So h(0.0) = 2.0 and h(1.0) = 1.0.
Most of the time, we use that feature to chain a Tween (defining the range of values of your animation) and a curveTween (defining a non linear interpolation).
// Common uses of the 'chain' feature
var fallAnimation = Tween(begin: 400.0, end: 0.0)
.chain(CurveTween(curve: Curves.bounceOut))
.animate(this.animationController);
But have you tried to chain other things ? Below are some examples.
Chaining details:
- Left : bounceIn curve chains decelerate flipped curve (I will note it bounceIn → decelerate flipped). The result is a a curve a little more flat at the beginning.
- Middle : the chain is more complex: easeInOutBack → Decelerate flipped → Interval (0.0, 0.5). It looks like an elasticIn curve, but without small bounces.
- Right : there i used the SawTooth curve, specific one that let you repeat X times another curve. In that case, elasticOut → easeInSine → SawTooth(3)
But I also encounter a limitation. Have you tried to chain an elasticOut or all other curve that transformed value is out of range [0.0, 1.0], like for example easeIn → elasticOut ?
It crashes, informing that value is out of range. More precisely, when the computed value of elasticOut is more than 1.0, then this value is given to the chained curve. But a Curve transform method only works on range [0.0, 1.0], guaranteed by an assertion in the source code.
T transform(double t) {
assert(t != null);
assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
return transformInternal(t);
}
With that issue (or should I say limitation), comes to me a question: is there other limitations related to animations in Flutter framework ?
Limits of the Framework : closed curves usecase
Another big question I had in mind was : why is there no sinus or cosinus curves? They are pretty easy to implements, and can produce nice effects to animations!
These two curves are “closed”, meaning that the beginning and the end are the same value. Don’t forget that we operate in one dimension, and as you may know, a sinus and a cosinus can be represented as a circle in 1D (only one parameter value change: the angle)!
So I decided to implement these two curves, respecting the previous limitation (curve are interpolated from 0.0 to 1.0).
class SinusCurve extends Curve {
@override
double transformInternal(double t) {
return 0.5 + math.sin(t*2*math.pi)*0.5;
}
}
Like this, I will be able to chain a sinus curve (respectively a cosinus curve) from any other curve.
First observation: sinus and cosinus curves always produce the first value to 0.0 and the last value to 1.0 (vertical lines at the beginning and the end). That’s not a problem in my curves themselves; this limitation can be observed in the source code for these classes: Curve, Interval, CurveTween. Concretely, even if Cosinus(0.0) should be 1.0, the framework shortcuts the computation and returns directly 0.0. Take a look to the code I retrieved from the framework:
// Curve class
@override
double transform(double t) {
if (t == 0.0 || t == 1.0) {
return t;
}
return super.transform(t);
}// CurveTween class
@override
double transform(double t) {
if (t == 0.0 || t == 1.0) {
assert(curve.transform(t).round() == t);
return t;
}
return curve.transform(t);
}// Interval class
@override
double transformInternal(double t) {
...
if (t == 0.0 || t == 1.0)
return t;
return curve.transform(t);
}
How to avoid that behavior ? My first thought was to create a counter part of all involved classes, in order to use closed curves (sinus, cosinus) the way I want. But think about it: to use them, you will have to create a ClosedCurveTween, maybe encapsulating your ClosedCurve in a ClosedInterval, all of this to generate a new kind of Animation object in order to cohabits with classic curves. In other words : we reinvent the wheel for that curves. Conclusion: it’s not worth it.
Added to that, think about usecases involving usage of sinus and cosinus curves. I were first convinced that we need those two curves, but the more I think about usecases, the less I think we need them. It can be used for shaking or rotating things, but you can also implements that with a combination of other curves, such as Tween ( from 0 to 2π), SequenceTween, SawTooth, et cetera…
Conclusion
We saw a lot of things about Curves in that article, and in that conclusion I will explain my thoughts about all that knowledge.
- Flipped curves : to be honest, I don’t use it for now. But if well done, my intuition is that it could be used to provide a nice reversed animation. Look at the flipped easeIn and easeOut curves: they seem identical (or at least very much similar).
- Curve range limitation : the conception is clearly made to ensure the consistency of the curve, and interval 0.0 to 1.0 is here to fix limits. We have to deal with it, because bypassing this constraints could lead to artifacts in your animations. For example, bounce curves have not been designed to be run outside range [0.0, 1.0].
- Framework shortcut : It’s a little bit annoying, because it implies that your curve must start to 0.0 and must finish to 1.0. I interpret that to reduce computation as possible, especially when the UI is refreshing a complete or dismissed animation. But I cannot find in the source code a comment about this. Why don’t Flutter devs kept transformation results for t=0.0 and t=1.0 ?
- Creating your own curve : unless they follow implicit rules described above, this is not a great idea. For example, you will have to redefine main classes if you want your curve to begin from 1.0 and end at 0.0. And also, there is a lot of curves already implemented, and for some, not accessible through Curves class. So before implementing your own curve, you should take a look to existing ones.
Thank you for reading this article. You can find all source code involved in that article below. You will be able to play with the curves viewer called “Curvy curves” :D