Flutter Animation Tutorial – Understand the Basics & Animate with Ease

4  comments

Apps are boring! Of course, only until you add good looking animations into them. Since Flutter renders everything to the screen itself without relying on those pesky native views, you can animate literally everything. As they say though, with great power comes great... confusion? In this tutorial, we will dispell this fog surrounding custom animations and you will finally gain clarity in this matter.

Multiple Ways of Animating

Much of the aforementioned confusion stems from the fact that there are many ways to do the same thing. Repeatedly calling setState in a StatefulWidget? Possible. What about extending an AnimatedWidget? Oh, and what's that AnimatedBuilder all about? Let's take a look at them one by one.

All of them will be demonstrated on a rotation animation.

Flutter's take on Animation

When you think about it, Flutter is built for animation. Very much like a game engine, it has a build method which can be called many times per second. If the widgets have some dynamically changing value, say for the rotation, then every time build is called, the widgets' rotation angle will change by a small amount. A Widget's rebuild can be invoked by calling setState.

basic_animation.dart

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Transform.rotate(
      angle: someChangingValue,
      child: Container(
        alignment: Alignment.center,
        padding: EdgeInsets.all(30),
        child: Image.asset(
          'assets/resocoder.png',
        ),
      ),
    ),
  );
}

But how can you change the rotation value in a way that produces a nice and smooth animation? This is where the animations come in.

When you think about animations, you probably think of some movement on the screen. This is not what the animation classes are about in Flutter. All of the movement and other eye candy is handled in the build method. Animations just generate nicely interpolated values multiple times per second.

Animation classes like Animation, AnimationController and Tween (more on them later) have essentially just one purpose - generate values which can be used in the build method.

Basics

Every custom animation begins with an AnimationController. It takes in a Duration and generates doubles with values between 0 (beginning) and 1 (end) in the specified time span. You can kick off this 0 to 1 animation with a forward method.

There's one additional step though - you have to tell this controller how many times per second it should progress toward the final value. For this, use a special ticker mixin on the widget containing the AnimationController. Depending on the device screen FPS, this ticker will "tick" 60 or maybe even 90 times per second. A new value will be generated on every tick.

basic_animation.dart

import 'package:flutter/material.dart';

class AnimationPage extends StatefulWidget {
  _AnimationPageState createState() => _AnimationPageState();
}

// Use TickerProviderStateMixin if you have multiple AnimationControllers
class _AnimationPageState extends State<AnimationPage>
    with SingleTickerProviderStateMixin
  AnimationController animController;

  @override
  void initState() {
    super.initState();
    animController = AnimationController(
      duration: Duration(seconds: 5),
      // This takes in the TickerProvider, which is this _AnimationPageState object
      vsync: this,
    );

    // Goes from 0 to 1, we'll do something with these values later on
    animController.forward();
  }

  @override
  Widget build(BuildContext context) {
    ...
  }

  @override
  void dispose() {
    animController.dispose();
    super.dispose();
  }
}
Always call dispose on the AnimationController to prevent memory leaks.

For using the values inside a widget, you need an Animation<double> instance which exposes a value property. Types other than double are, of course, also supported.

The AnimationController class is itself a subclass of Animation. So, while you can use the 0 to 1 values directly from the controller, you usually want to modify the range of these values. After all, what if you want values ranging from 0 to 500? And what about animating color changes from purple to green?

Tweens

Changing the value range and even the type of the output value (e.g. to a Color) is possible with a Tween. There are many pre-built ones such as ColorTween or TextStyleTween.

In the app we're building, we're going to use a simple Tween<double>. The value range of the animation should be a full circle for the rotation. Expressed in radians, it's 0 to 2π (0 to 360 degrees).

Tween takes in an Animation<double>, maps its progress to new values and returns a modified Animation. Store this returned Animation, in this case Animation<double>, in a field.

basic_animation.dart

...
class _AnimationPageState extends State<AnimationPage>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController animController;

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

    animation = Tween<double>(
      begin: 0,
      end: 2 * math.pi,
    ).animate(animController);

    animController.forward();
  }
  ...
}

Updating the UI

By now we have the final animation going from 0 to 2*PI and we even tell the AnimationController to start animating by calling the forward method. Two things are still missing though:

  1. Using the animation's value for the Transform's rotation.
  2. Calling setState to rebuild the UI when the animation value changes. This is possible with a listener.

basic_animation.dart

import 'package:flutter/material.dart';
import 'dart:math' as math;

class AnimationPage extends StatefulWidget {
  _AnimationPageState createState() => _AnimationPageState();
}

// Use TickerProviderStateMixin if you have multiple AnimationControllers
class _AnimationPageState extends State<AnimationPage>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController animController;

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

    animation = Tween<double>(
      begin: 0,
      end: 2 * math.pi,
    ).animate(animController)
      ..addListener(() {
        // Empty setState because the updated value is already in the animation field
        setState(() {});
      });

    animController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Transform.rotate(
        angle: animation.value,
        child: Container(
          alignment: Alignment.center,
          padding: EdgeInsets.all(30),
          child: Image.asset(
            'assets/resocoder.png',
          ),
        ),
      ),
    );
  }

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

Finally! We have a working visual animation! It's only going in one direction - forward. Let's make it loop infinitely.

Looping Infinitely

In addition to a regular listener which is updated on every newly generated value, there's also a statusListener. An animation can be in 4 stages:

  1. Dismissed - stopped at the beginning
  2. Forward - animating in forward direction (0 to 1)
  3. Completed - stopped at the end
  4. Reverse - animating from 1 to 0

Knowing about the current status of an animation is useful for many things, one of which is creating an infinite loop.

basic_animation.dart

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

  animation = Tween<double>(
    begin: 0,
    end: 2 * math.pi,
  ).animate(animController)
    ..addListener(() {
      // Empty setState because the updated value is already in the animation field
      setState(() {});
    })
    ..addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        animController.reverse();
      } else if (status == AnimationStatus.dismissed) {
        animController.forward();
      }
    });

  animController.forward();
}
...

It's important to still call animController.forward() outside of the statusListener, otherwise the animation won't begin.

Adding Curves

All of the animations are linear by default. This means that their speed is constant, no matter if they're at their beginning or end. Changing the curves to, for example, bounceIn and easeOut is very simple to do.

First, define a CurvedAnimation, its parent being the animController or any other Animation<double>, if you'd like. Then, animate the Tween using the newly created CurvedAnimation, not the controller directly.

basic_animation.dart

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

  final curvedAnimation = CurvedAnimation(
    parent: animController,
    curve: Curves.bounceIn,
    reverseCurve: Curves.easeOut,
  );

  animation = Tween<double>(begin: 0, end: 2 * math.pi)
      // Any animation can be passed in here, not just the AnimationController
      .animate(curvedAnimation)
  ...
}
...

The above approach is useful especially when you want to have different curves for forward and reverse animation. If this is not needed, you can use a CurveTween instead and then chain it with the other Tween.

basic_animation.dart

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

  // NO CURVED ANIMATION

  animation = Tween<double>(begin: 0, end: 2 * math.pi)
      // Chaining multiple Tweens will execute their "animation value modifications" sequentially
      .chain(CurveTween(curve: Curves.bounceIn))
      // Pass in the AnimationController directly again
      .animate(animController)
  ...
}
...

What's Next?

In this tutorial you gained an understanding of animations in Flutter. You now know how create fully-fledged animations using the classes which Flutter provides. The code, however, is quite messy. A little separation of concerns will surely help with code reuse and readability. In the next part, you will learn how to use AnimatedWidget and AnimatedBuilder to refactor your animations.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

  • Hello and thank you for your awesome tutorials.
    i have a question.
    what is the purpose of “drive” function for animations?

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >