If you follow the traditional AnimationController approach where you use setState
method to update the UI to show the animation, things might get slow when your build function is very large. When your build functon is very large and contains a lot of widgets, calling setState()
very frequently downgrade the performance as it rebuilds that large widget tree with each setState
call.
Woun't it be better if you take the animation part of your UI out of the main build function so that rest of the widget doesn't get rebuilt unnecessarily? AnimatedWidget is here to help you up.
To use AnimatedWidget
you need to extend this class and create another widget that is only reponsible for doing the animation. This class takes a Listenable
object as a required parameter. And you must overwrite the build
function. The mechanism of this widget is as it takes a Listenable
object, it will rebuilds its UI automatically whenever the widget get notified by that Listenable
object. Luckily the AnimationController
and Animation<T>
objects are derived from Listenable
, so you can use them.
Here is an example of animations from the previous article. Leter we will implement the same example with AnimatedWidget
.
class _TestState extends State<Test> with SingleTickerProviderStateMixin{ AnimationController _controller; Animation<double> opacityAnimation; Animation<double> widthAnimation; Animation colorAnimation; @override initState(){ super.initState(); _controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, ); opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _controller, curve: Interval(0.0, 1.0, curve: Curves.easeIn) ) ); widthAnimation = Tween<double>(begin: 0.0, end: 100).animate( CurvedAnimation( parent: _controller, curve: Interval(0.1, 0.4, curve: Curves.easeOut) ) ); colorAnimation = ColorTween(begin: Colors.yellow, end: Colors.red).animate( CurvedAnimation( parent: _controller, curve: Interval(0.4, 1.0, curve: Curves.easeOut) ) ); _controller.addListener((){ setState((){}); }); _controller.repeat(); } @override dispose(){ _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return BeautyBackground( child: Column( children: <Widget>[ BackButton(), Expanded( child: Center( child: SizedBox( width: (widthAnimation.value == null) ? 0.0 : widthAnimation.value, height: 100, child: Opacity( opacity : (opacityAnimation.value == null) ? 0.0 : opacityAnimation.value, child: DecoratedBox( decoration: BoxDecoration( color: (colorAnimation.value == null) ? Colors.red : colorAnimation.value, ), child: SizedBox( width: double.infinity, height: double.infinity ), ) ), ), ) ), ] ), ); } }
The above example animates three property, width, opacity and then the color of the box. We will need three class that will extend AnimatedWidget
. Each class to animate each property. So lets first consider the width of the box. Here is the implementation:
class WidthAnimation extends AnimatedWidget{ Widget child; WidthAnimation({Key key, AnimationController controller, this.child}) : super( key: key, listenable: Tween<double>(begin: 0.0, end: 100).animate( CurvedAnimation( parent: controller, curve: Interval(0.1, 0.4, curve: Curves.bounceIn), ) ) ); Animation<double> get _progress => listenable; @override Widget build(BuildContext context){ return SizedBox( width: _progress.value, height: 100, child: child ); } }
The subclass WidthAnimation
extends AnimatedWidget
class. The constructor this subclass must initialize its parent constructor and must pass listenable
object. Here are are creating a new Animation<double>
object using Tween
constructor and passing it as listenable
.
Now you should get the Animation
object using the following statement:
Animation<double> get _progress => listenable;
Next, you need to override the build
method that will return a widget. That's all about implementing a custom AnimatedWidget.
When you start the animation using the controller (using forward()
or repeat()
), the base class AnimatedWidget
will start generating values depending on the Listenable
object that was provided through the constructor. In our example, we have provided a Tween as a listenable object, and that Tween will start generating values from 0
to 100
, and will start at interval 0.1
and ends at 0.4
of the animation duration. When it generates a new value, it updates the listenable
object and calls build
method. So for each new value, the build function will be called and return the new UI. You do not need to call setState()
method here. It is done by the base class AnimatedWidget
automatically.
Now, we will do the same thing to animate other two property. Here is the code:
class OpacityAnimation extends AnimatedWidget{ Widget child; OpacityAnimation({Key key, AnimationController controller, this.child}) : super( key: key, listenable: Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: controller, curve: Interval(0.0, 1.0, curve: Curves.easeIn) ) ) ); Animation<double> get _progress => listenable; @override Widget build(BuildContext context){ return Opacity( opacity: _progress.value, child: child ); } } class ColorAnimation extends AnimatedWidget{ Widget child; ColorAnimation({Key key, AnimationController controller, this.child}) : super( key: key, listenable: ColorTween(begin: Colors.yellow, end: Colors.red).animate( CurvedAnimation( parent: controller, curve: Interval(0.4, 1.0, curve: Curves.easeOut) ) ) ); Animation<Color> get _progress => listenable; @override Widget build(BuildContext context){ return DecoratedBox( decoration: BoxDecoration( color: _progress.value ), child: child ); } }
We have now seperated out the animation part out of the main class _TestState
method. Here is the code that shows how you can combine the above three AnimatedWidget to perform the same thing:
class _TestState extends State<Test> with SingleTickerProviderStateMixin{ AnimationController _controller; @override initState(){ super.initState(); _controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, ); _controller.repeat(reverse: true); } @override dispose(){ _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return BeautyBackground( child: Column( children: <Widget>[ BackButton(), Expanded( child: Center( child: WidthAnimation( controller: _controller, child: OpacityAnimation( controller: _controller, child: ColorAnimation( controller: _controller, child: SizedBox( width: double.infinity, height: double.infinity, ), ) ), ), ) ), ] ), ); } }
Now your main class _TestState
looks more elegent and clean as we have totally moved the Animation declaration part out of the class and moved to its specific AnimatedWidget class. Now if you run the code your animation will be played but without calling the build
method by using setState()
. So no unncessary rebuild in the _TestState
class.
An important point you should know that, you should never put another AnimatedWidget
inside the build
method of a AnimatedWidget
. Becuase AnimatedWidget
calls build
method automatically and each time it calls build
method, it returns a new version of the UI. If you put another AnimatedWidget
inside the build
method, it will repeteadly create a new instance of the inner AnimatedWidget
every time the build
method is called. Here is the demonstration:
class WidthAnimation extends AnimatedWidget{ Widget child; AnimationController controller; WidthAnimation({Key key, this.controller, this.child}) : super( key: key, listenable: Tween<double>(begin: 0.0, end: 100).animate( CurvedAnimation( parent: controller, curve: Interval(0.1, 0.4, curve: Curves.bounceIn), ) ) ); Animation<double> get _progress => listenable; @override Widget build(BuildContext context){ return SizedBox( width: _progress.value, height: 100, child: OpacityAnimation( controller: controller, child: child, ) ); } } class OpacityAnimation extends AnimatedWidget{ Widget child; OpacityAnimation({Key key, AnimationController controller, this.child}) : super( key: key, listenable: Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: controller, curve: Interval(0.0, 1.0, curve: Curves.easeIn) ) ) ){ print("OpacityAnimation Initialized"); } Animation<double> get _progress => listenable; @override Widget build(BuildContext context){ return Opacity( opacity: _progress.value, child: child ); } }
In the above example, in the build
method the WidthAnimation
class, we have used OpacityAnimation
and passed the same controller that was given to the WidthAnimation
and passes the child as well. We have added a print
statement in the OpacityAnimated
constructor to see what happens when animation runs.
Now in the _TestState
class you will be able to use the code in the following way:
child: Center( child: WidthAnimation( controller: _controller, child: ColorAnimation( controller: _controller, child: SizedBox( width: double.infinity, height: double.infinity, ), ) ), )
Now the WidthAnimation
will be responsible for animating both width and opacity. The above code will work perfectly fine. But in the console, it will keep printing OpacityAnimation Initialized
during the whole time animation runs. You can guess why. Whenever the WidthAnimation
gets rebuilt, the OpacityAnimation
is being created anew. This will downgrade your application performace if you structure your animation like this way. So always avoid putting a AnimatedWidget
inside the build method of another AnimatedWidget
.
So it is always best practice to pass an AnimatedWidget
as a child to another AnimatedWidget
. Because when you pass the child, you passing an reference, so it doesn't get rebuilt.
The AnimatedWidget
doesn't require a child at all, if your child is relatively small, you can place it within the build
method of AnimatedWidget
. In this case you will only need to pass the animation controller to the AnimatedWidget
.