Flutter Animations Simplified III

Gonzalo Campos
5 min readMar 8, 2021

With implicit animations, we are creating code in a declarative manner. We tell them how to look like (instead of how to do it) by just writing the values we want them to take. Just as simple as that!

But what if we wanted, for example, a repeating animation? We’d need to write a lot of boilerplate involving the setState function to get what we want in our Flutter app.

In this article I’ll introduce the evil twin of implicit animations: explicit animations!

IMPORTANT: all the code present in this article is compatible with Flutter 2!

Level 2: Explicit Animations

Explicit Animations are just animations you have more control on. Unlike implicit animations, you’ll be writing imperative code, meaning that you’ll tell the animation what to do, and how to do it.

But first, let’s take a look to this animation:

Figure 1. Forever spinning Ditto.

How could we achieve this result by only using implicit animations? The solution I found was to use a TweenAnimationBuilder and Trasnform widgets (since there is no built-in rotation widget), and add 2π radians to the Ditto’s rotation angle every 5 seconds:

@override
void initState() {
super.initState();
// set timer
timer = Timer.periodic(Duration(seconds: 5), _setNewAngle);
}
void _setNewAngle(Timer t) {
setState(() {
// change Ditto's angle
angle += 2 * pi;
});
}

But I see some main problems with this code:

  • By using the Timer you are managing more than the animation values, which, in a certain way, would be breaking the declarative nature of implicit animations.
  • If you timer is not cancelled in the dispose method, there would be memory leaks in the app, because it would reference objects that no longer exist.
  • What if we want it to stop? We should cancel the timer, and if we want it to restart, we’d need to create a new timer, having a lot of objects for simple actions.

We can replace the timer with a kind of controller that solves all of this problems. And yeah, it already exists! Let me introduce the AnimationController:

class _CeciNestPasUnePipeState 
extends State<CeciNestPasUnePipe>
with SingleTickerProviderStateMixin {
late AnimationController controller;

@override
void initState() {
super.initState();
controller = AnimationController(
duration: Duration(seconds: 5),
vsync: this,
);
}

As you can see, the AnimationController receives a Duration, and a vsync parameter of the type TickerProvider. This code works because our widget uses the SingleTickerProviderStateMixin, which allows our widget behave like a TickerProvider.

In a few words, a Ticker is an object that will receive a callback and call it once per animation frame. So, if you app runs at 60fps, the callback will be called 60 times, once every frame.

The AnimationController has some self-explanatory methods that solve very intuitively the problems mentioned before:

controller.repeat();
controller.forward();
controller.stop();
controller.reverse();
controller.reset();

Something important to add is that you need to dispose your controller in the dispose method of the widget, in order to prevent memory leaks (that’s something we cannot get rid of):

@override
void dispose() {
controller.dispose();
super.dispose();
}

Enter Transition classes

Ok, we have our controller, but what can we do with it? What can we control? How can we use the controller to create a forever spinning Ditto? The Transition classes are exactly what we need! For simplicity, let’s take the RotationTransition widget:

@override
Widget build(BuildContext context) {
return RotationTransition(
child: Image.asset('assets/ditto.png'),
turns: controller,
);
}

The RotationTransition will receive a turns parameter of the type Animation<double>. Under the hood, AnimationController implements this interface, meaning that it will be interpolate between 0.0 and 1.0. But first, we’d need to launch our animation using the methods mentioned before:

@override
void initState() {
super.initState();
...
controller.repeat();
}

Technically, we have our forever spinning Ditto! But we can do a little bit more with our controller! Let’s add more functionalities:

void _stopOrStartAnimation() {
if (controller.isAnimating) {
controller.stop();
} else {
controller.repeat();
}
}
void _invertAnimationDirection() {
if (controller.status == AnimationStatus.reverse) {
controller.repeat();
} else if (controller.status == AnimationStatus.forward) {
controller.reverse();
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _stopOrStartAnimation,
onLongPress: _invertAnimationDirection,
Figure 2. RotationTransitionDitto in action.

We can check the status of the controller, we can check if it is animating, if it is going backwards, if the animation was completed or dismissed, and based on that information we can tell the controller what’s the next action to do: imperative code.

Curved Animations

There are a lot of built-in transition widgets (we can even create our own ones, but that’s for the next chapter). But some of them, instead of using Animation<double> objects (like the AnimationController), use, for example, Animation<RelativeRect> or Animation<Rect> .

In previous chapters we talked about the important role of interpolation. Under certain conditions, we can interpolate almost any type of data possible (we’ve already done it). The Tween classes allowed us to do that, but they also allow us to create animations objects based on those values!

Taking a look to the SlideTransition, it receives an Animation<Offset> object. We can create a Tween<Offset> and use the magical method animate to create our animation:

late Animation<Offset> offsetAnimation;...offsetAnimation = Tween(
begin: Offset.zero,
end: Offset(0.0, 1.5),
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.linear,
),
);
...@override
Widget build(BuildContext context) {
return SlideTransition(
child: Image.asset('assets/ditto.png'),
position: offsetAnimation,
);
}

The animate method receives a CurvedAnimation, which is described as:

An animation that applies a curve to other animation

So, for short, we are applying a linear curve to the controller (which is an animation from 0.0 to 1.0), but taking the values of the Tween object, which are Offset values, and, in consequence, we’ll have the Animation<Offset> object we need.

Figure 3. SlideTransitionDitto in action.

We can even apply different curves to our curved animation and have a more interesting animation:

Figure 3. Animation<Offset> with Curves.easeInOutCubic

Conclusion

We have more control on our animations with less boilerplate and more readable code. However, it’s importante to distinguish if we need either implicit or explicit animations. Because both solutions can solve the same problems but fit differently in our use cases.

Do not forget to star the project repo. ;)

--

--