About Flutter Animation

Flutter animates widget by repeteadly calling build function very frequently. Almost 60 times per second. And every time the build function is called, the widget is redrawn on the screen. For example, if you want to animate width from 300px to 400px, the first build function will be called with the value 300, and then next build function will be called with the value 301 and so on. Frequent changing on the width over a certain period of time makes the UI looks like animating. This is the core mechanism how animation works. You will constantly change the value of a property you want to animate over a certain period of time.

In this tutorial we will learn how to change the value over a certain period of time.

Flutter animation has three main component.

Ticker

In simple words, a Ticker is a class which sends a signal at almost regular interval (around 60 times per second). Think of your watch which ticks at each second. At each tick, the Ticker invokes callback method(s) with the duration since the first tick, after it was started. To use Ticker you need to implement SingleTickerProviderStateMixin class. Here is a full example:

class _TestState extends State<Test> with SingleTickerProviderStateMixin{

  AnimationController _controller;

  @override
  initState(){
    super.initState();
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 1000), 
      vsync: this,
    );
  }

  @override
  dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return BeautyBackground(
      child: Column(
        children: <Widget>[
          BackButton(),
          Expanded(
            child: Center(
              child: SizedBox(
                width: 100,
                height: 100,
                child: Opacity(
                  opacity : 1,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      color: Colors.pink,
                    ),
                    child: SizedBox(
                      width: double.infinity,
                      height: double.infinity
                    ),
                  )
                ),
              ),
            )
          ),
        ]         
      ),
    );
  }
}

We will use this example throughout this tutorial. In the above example our state class implements SingleTickerProviderStateMixin, and that makes the state class attached to the ticker. The vsync property of the AnimationController constructor is used to provide the ticker.

If you implement SingleTickerProviderStateMixin, you can use only one AnimationController in the class. If you want to use multiple AnimationController you must implements TickerProviderStateMixin instead of SingleTickerProviderStateMixin. If your class have only one AnimationController it is better and efficient to use SingleTickerProviderMixin.

Animation

An Animation is nothing else but a value (of a specific type) that can change over the lifetime of the animation. The way the value changes over the time of the animation can be linear (like 1, 2, 3, 4, 5…) or much more complex (see Curves, later).

AnimationController

An AnimationController is a class that controls (start, stop, repeat…) an animation (or several animations). In other words, it makes the Animation value vary from a lowerBound to an upperBound in a certain duration, using a velocity (= rate of change of value per second).

AnimationController

Lets take a look at the AnimationController constructor:

AnimationController controller = new AnimationController(
  value:    // the current value of the animation, usually 0.0 (= default)
  lowerBound: // the lowest value of the animation, usually 0.0 (= default)
  upperBound: // the highest value of the animation, usually 1.0 (= default)
  duration: // the total duration of the whole animation (scene)
  vsync:    // the ticker provider
  debugLabel: // a label to be used to identify the controller
      // during debug session
);

Most of the time, value, lowerBound, upperBound and debugLabel are not mentioned when initializing an AnimationController.

In our example, we have,

_controller = new AnimationController(
  duration: const Duration(milliseconds: 1000), 
  vsync: this,
);

The line vsync: this binds the ticker with the animation controller. As we haven't provided lowerBound and upperBound, the default will be 0.0 and 1.0 respectively. So in our example, the controller will generate the values from 0.0 to 1.0 during 1000 milliseconds in a linear way. But how to get the generated value? You need attach a listener to the animation controller in order to get the generated values.

@override
  initState(){
    super.initState();
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 1000), 
      vsync: this,
    );
    _controller.addListener((){
      print(_controller.value);
    });
    _controller.forward();
  }

In the above example, we have attached a listener to the controller. The provided callback will be called every time the ticker sends a signal and controller will be updated with the new value and the the controller will call the listener callback. To get the value use value property of the animation controller.

_controller.value

The above code will keep printing the generated values for whole 1000 milliseconds.

flutter: 0.033333
flutter: 0.083333
flutter: 0.1
flutter: 0.116667
flutter: 0.133333
flutter: 0.15
flutter: 0.166667
flutter: 0.183333
flutter: 0.2
flutter: 0.216667
flutter: 0.233333
flutter: 0.25
flutter: 0.266667
flutter: 0.283333
flutter: 0.3
flutter: 0.316667
flutter: 0.35
flutter: 0.366667
flutter: 0.383333
flutter: 0.4
flutter: 0.416667
flutter: 0.433333
flutter: 0.45
flutter: 0.466667
flutter: 0.483333
flutter: 0.5
flutter: 0.516667
flutter: 0.533333
flutter: 0.55
flutter: 0.566667
flutter: 0.583333
flutter: 0.6
flutter: 0.616667
flutter: 0.633333
flutter: 0.65
flutter: 0.666667
flutter: 0.683333
flutter: 0.7
flutter: 0.716667
flutter: 0.733333
flutter: 0.75
flutter: 0.766667
flutter: 0.783333
flutter: 0.8
flutter: 0.816667
flutter: 0.833333
flutter: 0.85
flutter: 0.866667
flutter: 0.883333
flutter: 0.9
flutter: 0.916667
flutter: 0.933333
flutter: 0.95
flutter: 0.966667
flutter: 0.983333
flutter: 1.0
flutter: 1.0

As you can see, it generated various value within 1000 milliseconds.

Example: Animating Opacity

The statement _controller.forward() starts the animation. When the control flow mets this statement, the animation controller starts generating the values from lowerbound to upperbound. We have put this statement within the initState() method. That means whenever the widget is painted it immediately starts generating values. You can put this statement within a button so that on button click the animation will start.

Now we will use this value to change the opacity of the box. In our example, we have wraped the content of SizedBox widget within the Opacity widget with the opacity given 1. Now we will change the opacity dynamically with the help of animation controller to make it animate.

class _TestState extends State<Test> with SingleTickerProviderStateMixin{

  AnimationController _controller;

  double animatedOpacity = 0;

  @override
  initState(){
    super.initState();
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 1000), 
      vsync: this,
    );
    _controller.addListener((){
      setState((){
        animatedOpacity = _controller.value;
      });
    });
    _controller.forward();
  }

  @override
  dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return BeautyBackground(
      child: Column(
        children: <Widget>[
          BackButton(),
          Expanded(
            child: Center(
              child: SizedBox(
                width: 100,
                height: 100,
                child: Opacity(
                  opacity : animatedOpacity,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      color: Colors.pink,
                    ),
                    child: SizedBox(
                      width: double.infinity,
                      height: double.infinity
                    ),
                  )
                ),
              ),
            )
          ),
        ]         
      ),
    );
  }
}

In the above example, we have taken a variable animatedOpacity and assigned it to the opacity property instead of a static value. And whenever the animation controller generates a value, we are calling setState() method and assigning the new generated value to the variable. The setState() method causes the build function to rebuild the widget with the new value.

Example: Animating Width

In the following example, we are animating the width, we have also set lowerBound and upperBound to animate the width.

class _TestState extends State<Test> with SingleTickerProviderStateMixin{

  AnimationController _controller;
  double animatedWidth = 100;

  @override
  initState(){
    super.initState();
    _controller = new AnimationController(
      lowerBound: 100,
      upperBound: 300,
      duration: const Duration(milliseconds: 1000), 
      vsync: this,
    );
    _controller.addListener((){
      setState((){
        animatedWidth = _controller.value;
      });
    });
    _controller.forward();
  }

  @override
  dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return BeautyBackground(
      child: Column(
        children: <Widget>[
          BackButton(),
          Expanded(
            child: Center(
              child: SizedBox(
                width: animatedWidth,
                height: 100,
                child: Opacity(
                  opacity : 1,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      color: Colors.pink,
                    ),
                    child: SizedBox(
                      width: double.infinity,
                      height: double.infinity
                    ),
                  )
                ),
              ),
            )
          ),
        ]
      ),
    );
  }
}

In the above example, the lowerBound is set to 100 and upperBound is set to 300, that means the generated value will be from 100 to 300. The controller will keep generating the values from 100 to 300 for whole 1 seconds and using the setState() we are updating the UI frequently to turn it into animation.

AnimationController Properties

value

Using the value property you can get the current value of the animation.

_controller.value

status

The property status represents the current animation status. The status is represented by the AnimationStatus enum. This enum defines the following four constants:

AnimationController Methods

.forward()

This method starts the animation. That means it starts generating values from lowerBound to upperBound.

.repeat()

This method will repeat the animation forever. It will start from lowerBound to upperBound, and when the animation completes, it will again repeat from lowerBound to upperBound. That means it will repeat the animation always in forward direction.

_controller.repeat();

It takes an optional argument reverse, if set to true, the animation will start from lowerBound to upperBound, and then upperBound to lowerBound and then it will repeat the cycle. That means the animation will be played in forward and reverse direction both.

_controller.repeat(reverse: true);

This method also accepts min and max to set the lowerBound and upperBound value respectively. Without these arguments the controller will use lowerBound and upperBound. min and max property is used to override these two value.

_controller.repeat(reverse: true, min: 100, max: 150);

.reverse({from: double})

Starts running this animation in reverse (towards the beginning). Means it will start the animation from current value to lowerBound, in reverse direction.

_controller.reverse();

It is important to note that, it starts from current value to lowerBound, not upperBound to lowerBound, so if your current value is equal to lowerBound, you won't see any animation.

This method has optional parameter from which you can use to set the current value from which the animation will start.

_controller.reverse(from: 300);

.dispose()

Release the resources used by this object. The object is no longer usable after this method is called.

_controller.dispose();

.stop()

Stops running this animation.

_controller.stop();

addListener()

You can attach a callback(listener) with the controller. The callback will be called each time the controller changes its value. The method addListener() takes one argument which is the callback. The callback doesn't have any parameter.

_controller.addListener((){
  print("New value : " + _controller.value.toString());
});

Normally you would change the state so that the UI gets rebuild:

_controller.addListener((){
  setState((){});
});

addStatusListener()

The method addStatusListener() takes a callback as a parameter. This callback is called each time the controller changes its animation status. The callback has one parameter which is AnimationStatus that represents the current status of the animation.

_controller.addStatusListener((AnimationStatus status){
  switch(status){
    case AnimationStatus.forward:
      print("Animation is running in forward mode !");
      break;
    case AnimationStatus.reverse:
      print("Animation is running in reverse mode !");
      break;
    case AnimationStatus.completed:
      print("Animation has been completed !");
      break;
    case AnimationStatus.dismissed:
      print("Animation is dismissed before it began !");
      break;
    default:
      print("Don't know what to say !");
  }
});