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.
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
.
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).
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).
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.
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.
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.
Using the value
property you can get the current value of the animation.
_controller.value
The property status
represents the current animation status. The status is represented by the AnimationStatus
enum. This enum defines the following four constants:
completed
: The animation is stopped at the end.dismissed
: The animation is stopped at the beginning.forward
: The animation is running from beginning to end.reverse
: The animation is running backwards, from end to beginning.This method starts the animation. That means it starts generating values from lowerBound to upperBound.
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);
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);
Release the resources used by this object. The object is no longer usable after this method is called.
_controller.dispose();
Stops running this animation.
_controller.stop();
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((){}); });
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 !"); } });