How to use Flutter Animations API

In this blog, we talk about how flutter is smart enough in making good use of the obtained elapsed time from Ticker API.

GraphQL has a role beyond API Query Language- being the backbone of application Integration
background Coditation

How to use Flutter Animations API

In Flutter’s Ticker API, we achieved the first step of getting the latest value of elapsed time whenever it changes, through Flutter’s Ticker API.

Curves

Flutter offers a Curve abstract class which comes with transform(double t) which is the curve function representing the curve class. The transform method is used to get different values of the curve at different provided values of t.

Flutter sets some restrictions on the transform method to

  • the domain of accepted values for parameter t is 0.0 to 1.0 inclusive
  • the return value of the method will always lie within a range of 0.0 to 1.0 inclusive
  • return 0.0 when t passed to it is 0.0
  • return 1.0 when t passed to it is 1.0

Through these restrictions, flutter is trying to communicate that the curves which are being represented through the subclasses of Curve will always have mappings of t from 0.0 to 1.0 to return values within the range of 0.0 to 1.0. If you consider the below empty graph, the rectangular region is where the graph of any transform function would lie.

There are many in-built curves supported by flutter out of the box, making it easy to use some of the most commonly used curves like Curves.bounceIn, Curves.bounceOut, Curves.easeIn, and many more.

To have a look at the complete list of curves supported by Flutter, you can visit Curves classes by Flutter.

Using Curves with Ticker

After getting some idea about what Flutter has to offer regarding curve functions, we can finally talk about the last step which we need to achieve the animation using Flutter.
By the end of the previous part of Flutter’s Ticker API, we discussed the ticker callback provided by Flutter’s Ticker API which gets called with a fresh new instance of elapsed time, after every new frame is being rendered by the underlying rendering engine of flutter.


_ticker = this.createTicker(
  (Duration elapsed) {
		// Here we would want to reach out to our desired curve function
    // and obtained the value of the curve after `elapsed` amount of time has been elapsed
  },
);

But we found from the above Curves section that every curve function offered by Flutter has some restrictions regarding the domain and range of the function, which will have to obey to make use of curve functions.

Here, when Curve.transform(t) method restricts the allowed values of t from 0.0 to 1.0, it wants to convey that it expects the percentage of time elapsed, instead of the actual time elapsed in duration.

Thus, here we would want to convert the elapsed time obtained from the ticker’s callback into the percentage of time elapsed concerning the total desired duration of the animation.

Thus, if we want the duration of an animation to be of around Duration(seconds: 5), and for instance, the ticker’s callback is invoked with an elapsed time of around Duration(seconds: 2), the Curves.transform(t) method expects the percentage of time elapsed which would be elapsedDurationInSeconds / totalDurationInSeconds.


final totalAnimationDuration = Duration(seconds: 5);
_ticker = this.createTicker(
  (Duration elapsed) {
		final totalDurationInSeconds =
        totalAnimationDuration.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    final elapsedDurationInSeconds =
        elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;

    double percentageOfTimeElapsed = elapsedDurationInSeconds / totalDurationInSeconds;

    if (percentageOfTimeElapsed >= 1) {
			// Indicates that animation has completed
      _ticker.stop();
    }

		// Ensuring to meet restrictions by Curves.transform(t)
		percentageOfTimeElapsed = percentageOfTimeElapsed > 1.0 ? 1.0 : percentageOfTimeElapsed;
		percentageOfTimeElapsed = percentageOfTimeElapsed < 0.0 ? 0.0 : percentageOfTimeElapsed;

    final value = Curves.bounceOut.transform(percentageOfTimeElapsed);
  },
);

Now, we will get a new value inside the ticker’s callback, at every change in elapsed time. The value stored inside the variable value will always be between 0.0 to 1.0 inclusive.

Our next goal should be to use the obtained value to obtain the corresponding value of the widget’s property.

Consider a widget that is a simple Container with color: Colors.black, and we would want to animate its height property from 0px to 100px over 5 seconds.


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox > createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox>
    with SingleTickerProviderStateMixin {
  late final Ticker ticker;
  static const totalAnimationDuration = Duration(seconds: 5);
  double curveValue = 0.0;

  @override
  void initState() {
    initializeAnimation();
		handleForward();
    super.initState();
  }

  void initializeAnimation() {
    ticker = createTicker((elapsed) {
    final totalDurationInSeconds =
        totalAnimationDuration.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    final elapsedDurationInSeconds =
        elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;

    double percentageOfTimeElapsed = elapsedDurationInSeconds / totalDurationInSeconds;

    if (percentageOfTimeElapsed >= 1) {
			// Indicates that animation has completed
      ticker.stop();
    }

		// Ensuring to meet restrictions by Curves.transform(t)
		percentageOfTimeElapsed = percentageOfTimeElapsed > 1.0 ? 1.0 : percentageOfTimeElapsed;
		percentageOfTimeElapsed = percentageOfTimeElapsed < 0.0 ? 0.0 : percentageOfTimeElapsed;

    final value = Curves.bounceOut.transform(percentageOfTimeElapsed);
      setState(() {
        curveValue = value;
      });
    });
  }

  void handleForward() {
    ticker.start();
  }

  void handleReset() {
		setState(() {
      curveValue = 0.0;
    });
    ticker.stop();
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100.0 * curveValue,
      width: 100.0,
      color: Colors.black,
    );
  }
}

Here, we are using the range of values of the curve and converting it to the range of heights of the container. i.e. We want the height of the container to change from 0 to 100px, thus if we multiply 100.0 with every changing value of the curve, the value of the height will range from 0 to 100.0 because the value of the curve will always range from 0.0 to 1.0.

We can use this strategy to change any widget’s property over a range of values. For instance, if we want to rotate a widget by an angle of pi/2, we would want to multiply pi/2 with every changing value of the curve.


	... // rest of the code need not change
  ...

	@override
  Widget build(BuildContext context) {
    return Transform.rotate(
      angle: pi / 2 * curveValue,
      child: Container(
        height: 100.0,
        width: 20,
        color: Colors.black,
      ),
    );
  }
}

Thus, we finally achieved the last step to create animations with flutter, which was using the elapsed time obtained inside the ticker’s callback on every new frame, and using the elapsed time to compute the next curve’s value and in turn it to determine the current value of widget’s property.

Although this might seem good enough to go with, flutter has a set of dedicated helper classes just to achieve the same as we tried to in the above implementation. These set of animation helpers combined are referred to as Animation API. Let’s have a look at it next.

Using Flutter’s Animations API

In the above section, we discussed how to use Ticker’s API along with Curve to implement the desired curved animation we would want to update the value of a widget’s property.

Flutter offers many helper abstractions which pretty much achieve the same thing which we did in the initializeAnimation method in the above section. These helper abstractions together are referred to as Flutter’s Animation API.

Flutter exposes AnimationController which internally sets up the Ticker similar to the way we did above.

It has several other helper methods which are dedicated to helping easily achieve more control over changes in values of curves with changes in elapsed time obtained from the ticker’s callback. Some of the most commonly used methods are:

  • double get value: refers to the current value of the curve at a particular instance of time during the animation. This value is the same as what we were getting as the return value from Curves.bounceOut.transform(percentageOfTimeElapsed)
  • void forward(): When invoked, it starts the animation, i.e. it would start the ticker to start listening to new frames of flutter and call the ticker’s callback defined internally within the AnimationController. This method is similar to what we achieved from doing ticker.start() from within handleForward method of the widget’s state class.
  • void reset(): When invoked, it stops the current animation, i.e. unsubscribes from getting notified of every new frame, and starts a new subscription for the same along with resetting so far reached curve value to 0.0. This method is similar to what we tried to achieve from within the handleReset method i.e. invoking the ticker.stop() along with resetting the so-far reached curveValue to 0.0.
  • void addListener(void Function() listener): This method allows to subscribe and get notified whenever the value of the curve changes, which happens whenever the ticker’s callback is called and the internal animation controller updates the value of the curve using the new elapsed time obtained. This method can be used to update our state with the latest controller’s value.
  • void dispose(): used to dispose of all the resources used by the animation controller to dispose of the underlying ticker’s instance being used, which we did in the previous example from within the state’s disposal method.

To instantiate AnimationController, it has a default constructor which takes in a required parameter vsync which is of type TickerProvider. To achieve this, we would need to mix in the state class of the widget with either of SingleTickerProviderStateMixin or TickerProviderStateMixin, which turns the state class of the widget into a class that has the capability of a provider ticker.

Let’s replace the implementation of animation using AnimationController:


class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox > createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State <AnimatedBox>
    with SingleTickerProviderStateMixin {
  late final AnimationController controller;
  static const duration = Duration(seconds: 5);
  double curveValue = 0.0;

  @override
  void initState() {
    initializeAnimation();
    super.initState();
  }

  void initializeAnimation() {
    controller = AnimationController(vsync: this, duration: duration);
    controller.addListener(() {
      setState(() {
        curveValue = controller.value;
      });
    });
  }

  void handleForward() {
    controller.forward();
  }

  void handleReset() {
    controller.reset();
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100.0 * curveValue,
      width: 100.0,
      color: Colors.black,
    );
  }
}

One thing we missed is letting the Animation’s API know about which curve we would want to use. By default, AnimationController uses Curves.linear curve. Flutter’s Animation API offers a special class just for this use case, the CurvedAnimation, which is very well designed to decouple the dependency of the curve from AnimationController from which curve to be used.

CurvedAnimation also exposes similar methods as of AnimationController. It has a default constructor which takes in a required parameter of the parent of type AnimationController along with the parameter curve of type Curve.

CurveAnimation’s sole purpose is to convert the value of the AnimationController to the value which corresponds to the curve for that particular value of elapsed time.

Thus, mostly all of the code will be the same, just we would want to update curveValue with the value obtained from CurvedAnimation.


class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox>
    with SingleTickerProviderStateMixin {
  late final AnimationController controller;
  late final Animation<double> bounceOutAnimation;
  static const duration = Duration(seconds: 5);
  double curveValue = 0.0;

  @override
  void initState() {
    initializeAnimation();
    super.initState();
  }

  void initializeAnimation() {
    controller = AnimationController(vsync: this, duration: duration);
    bounceOutAnimation = CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    bounceOutAnimation.addListener(() {
      setState(() {
        curveValue = bounceOutAnimation.value;
      });
    });
  }
  void handleForward() {
    controller.forward();
  }
  void handleReset() {
    controller.reset();
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100.0 * curveValue,
      width: 100.0,
      color: Colors.black,
    );
  }
}

If you have been working with Flutter for quite a while now, you might have seen something like this already, but now after having to understand all of the behind-the-scenes stuff that goes under the hood, this should not seem a mystery anymore.

It is quite normal if you are feeling something like this right now if you are new to flutter. Every Flutter dev has been through this phase at least once!

One more thing which was quite bothering to Flutter people was this very pattern of keeping track of updated curveValue. This pattern was so common to be able to achieve curved animations in a flutter that they have a dedicated widget AnimatedBuilder useful to get rid of maintaining yet another state value just for keeping track of updated curveValue.

Using AnimatedBuilder

AnimatedBuilder is a widget that takes in the instance of AnimationController or CurvedAnimation to listen for and gives a builder callback, which gets called whenever there’s a new value of the animation.

Thus, we need not maintain a subscription on an instance of either AnimationController or CurvedAnimation just to set the new value of curvedValue. We could directly refer .value property of either AnimationController or CurvedAnimation from inside the widget. Which eliminates the need for maintaining the state curvedValue.


class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox>
    with SingleTickerProviderStateMixin {
  late final AnimationController controller;
  late final Animation<double> bounceOutAnimation;
  static const duration = Duration(seconds: 5);

  @override
  void initState() {
    initializeAnimation();
    super.initState();
  }

  void initializeAnimation() {
    controller = AnimationController(vsync: this, duration: duration);
    bounceOutAnimation = CurvedAnimation(parent: controller, curve: Curves.bounceOut);
  }

  void handleForward() {
    controller.forward();
  }

  void handleReset() {
    controller.reset();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: bounceOutAnimation,
      builder: (context, child) {
        return Container(
          height: 100.0 * bounceOutAnimation.value,
          width: 20,
          color: Colors.black,
        );
      },
    );
  }
}

Yeah, flutter always knows what you want in the end! and yeah, flutter is not done yet!

If this was not all, flutter found this pattern to be very common as well to change container’s properties through a curved animation over a particular duration. Thus, flutter offers AnimatedContainer widget to achieve the same and even more than that. It is an animated version of Container that gradually changes its values over some time.

The AnimatedContainer will automatically animate between the old and new values of properties when they change using the provided curve and duration. Null properties are not animated. Its child and descendants are not animated. This class is useful for generating simple implicit transitions between different parameters to Container with its internal AnimationController.

Conclusion

There are also many widgets offered by Flutter which are wrappers around several common animation patterns. You can have a look at all of them here.

The core basics behind all these widgets are what we have been discussing in this series of articles. All these widgets use the Ticker API under the hood along with their own set of optimizations. From here on, you must be able to imagine what actually happens behind the scenes and thus readily get used to using the abstractions offered by Flutter.

This brings us to conclude this 3 part series of articles on diving deep into animations in a flutter.

  • In part 1, we saw exactly what are animations irrespective of considering any UI framework in specific. We spoke about what it takes for the human eye to believe that there’s an actual animation taking place on a particular object or widget along with the two important points which we need to work on to bring animation effect

                       - elapsed time: how can we keep on getting the fresh instance of elapsed time as and when elapsed time changes, since the animation has started?
                      - curve
: Which curve to use to compute the next value of the widget’s property(height) given an elapsed time.

  • In part 2 and this blog, we spoke about how we can achieve these two points using Flutter. We saw the backbone behind any animation in Flutter which is the Ticker API, and how it can be used to achieve fresh instances of elapsed time.
  • In this blog, we saw how we can make use of Ticker API together with Curves classes offered by the Flutter framework, to achieve curved animations. In the end, I showed how the flutter’s Animation API under the hood also makes use of Ticker API and removes all the hard work from the developers through their several abstractions being offered like AnimationController, CurvedAnimations, AnimatedBuilder and many helper widgets like AnimatedContainer, which have been developed to cover most common use cases for developing UI animations in a flutter.

I hope this 3 part series helped connect the dots well to get a deeper understanding of what happens under the hood.

Hi, I am Vishal Govind, a passionate software developer, fluent in Dart/Flutter along with experience in working with front-end technologies like React-native. I enjoy exploring the latest technologies or playing drums/keyboard when I am not working.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.