For a front-end App, adding appropriate animation can give users a better experience and visual effect. Therefore, whether it is native IOS or Android, or in front-end development, it will provide an API for dictation animation
Flutter has its own rendering closed loop. Of course, we can provide it with a small data model to help us achieve the corresponding animation effect
1. Understanding of animation API
In fact, Animation is that we provide different values to the fluent engine in some ways (such as objects and Animation objects), and fluent can add smooth effects to the corresponding Widget according to the values we provide.
1.1 Animation
In fluent, the core class to realize Animation is Animation. Widget s can directly combine these animations into their own build methods to read their current values or monitor their state
Let's take a look at the class Animation, which is an abstract class:
- addListener method
- Whenever the animation state value changes, the animation notifies all listeners added through addListener
- Usually, a state object listening for animation will call its own setState method to
- addStatusListener
- When the state of the animation changes, all listeners added through addStatusListener are notified
- Normally, the animation will start from the lost state, indicating that it is at the starting point of the change interval
- For example, an animation from 0.0 to 1.0 should have a value of 0.0 in the dismissed state
- Finally, if the animation reaches the end point of its interval (such as 1.0), the animation will become completed
abstract class Animation<T> extends Listenable implements ValueListenable<T> { const Animation(); // Add animation listener @override void addListener(VoidCallback listener); // Remove animation listener @override void removeListener(VoidCallback listener); // Add animation status listener void addStatusListener(AnimationStatusListener listener); // Remove animation state listener void removeStatusListener(AnimationStatusListener listener); // Gets the current state of the animation AnimationStatus get status; // Gets the current value of the animation @override T get value;
1.2 AnimationController
Animation is an abstract class and cannot be used to directly create objects to realize the use of animation.
AnimationController is a subclass of Animation. To realize Animation, we usually need to create
AnimationController object:
- The animation controller generates a series of values. By default, the values are in the range of 0.0 to 1.0
In addition to the above monitoring to obtain the status and value of animation, AnimationController also provides control over animation:
- Forward: executes the animation forward
- reverse: play the animation in the opposite direction
- stop: stop Animation
Source code of AnimationController:
class AnimationController extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { AnimationController({ // initialize value double value, // Animation execution time this.duration, // Time the inverse animation was executed this.reverseDuration, // minimum value this.lowerBound = 0.0, // Maximum this.upperBound = 1.0, // Callback of refresh rate ticker (see the detailed analysis below) @required TickerProvider vsync, }) }
AnimationController has a required parameter vsync. What is it?
- I talked about the closed-loop rendering of shutter before. Shutter needs to wait for a vsync signal before rendering a frame every time.
- This is also to monitor the vsync signal. When the application developed by Flutter no longer accepts the synchronization signal (such as locking the screen or retreating to the background), continuing to execute the animation will consume performance
- At this time, we set up Ticker, so we won't start animation again
- In development, it is common to mix SingleTickerProviderStateMixin into the definition of State.
1.3 CurvedAnimation
CurvedAnimation is also an implementation class of Animation. Its purpose is to add Animation curves to AnimationController:
- CurvedAnimation can combine AnimationController and Curve to generate a new Animation object
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> { CurvedAnimation({ // Usually an AnimationController is passed in @required this.parent, // Objects of type Curve @required this.curve, this.reverseCurve, }); }
There are some constant Curves (the same as some Colors of Color type) for objects of Curve type, which can be used directly by us:
- For the effect of the corresponding value, you can directly view the official website (there is a corresponding gif effect, which is clear at a glance)
- https://api.flutter.dev/flutter/animation/Curves-class.html
The official also gives an example of defining curve:
import 'dart:math'; class ShakeCurve extends Curve { @override double transform(double t) => sin(t * pi * 2); }
1.4 Tween
By default, the range of values generated by animation controller animation is 0.0 to 1.0
- If you want to use values other than this or other data types, you need to use Tween
Tween's source code: the source code is very simple. You can pass in two values and define a range.
class Tween<T extends dynamic> extends Animatable<T> { Tween({ this.begin, this.end }); }
Tween also has some subclasses, such as ColorTween and BorderTween, which can set animation values for animation or borders
Tween.animate
To use the Tween object, you need to call Tween's animate() method and pass in an Animation object.
2. Animation Practice
2.1 use of basic animation
Case: click the play button in the lower right corner, and the heart widget in the center of the screen will zoom back and forth
class GYHomePage extends StatefulWidget { @override _GYHomePageState createState() => _GYHomePageState(); } /** * Animation creation must be inherited from StatefulWidget * First, the AnimationController needs to be mixed with the SingleTickerProviderStateMixin class */ class _GYHomePageState extends State<GYHomePage> with SingleTickerProviderStateMixin { //Create AnimationController AnimationController? _animationController; //Must be initialized or an optional type defined CurvedAnimation? _curvedAnimation; Animation? _sizeAnimation; @override void initState() { // TODO: implement initState super.initState(); //1. Create AnimationController _animationController = AnimationController(vsync: this, duration: Duration(seconds: 2)); //2. Set the value of Curved (for example, uniform speed, some effects) _curvedAnimation = CurvedAnimation(parent: _animationController!, curve: Curves.linear); //3. Set Tween (function: change the size of animation value, because by default, the animation value of AnimationController is [0.0, 1.0]. If you want to use other values, you must use Tween) // The type of the two values of Tween(begin: 50, end: 150) is T, that is, generic, but actually we need to be double. These write 50 are Int by default, so some 50.0 is required here _sizeAnimation = Tween(begin: 50.0, end: 150.0).animate(_curvedAnimation!); //Associate values with animation /*Monitor animation execution*/ _animationController!.addListener(() { setState(() { }); }); //Monitor animation execution status _animationController!.addStatusListener((status) { if (status == AnimationStatus.completed) { _animationController!.reverse(); } else if (status == AnimationStatus.dismissed) { _animationController!.forward(); } }); } @override Widget build(BuildContext context) { // TODO: implement build return Scaffold( appBar: AppBar(title: Text("home page"),), body: Center( child: Icon(Icons.favorite, size: _sizeAnimation!.value,color: Colors.red,), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_circle_outline), onPressed: () { print("Play animation button click event======="); if (_animationController!.isAnimating) { _animationController!.stop(); } else { if (_animationController!.status == AnimationStatus.forward) { /*Start animation*/ _animationController!.forward(); } else if (_animationController!.status == AnimationStatus.reverse) { /*Start animation*/ _animationController!.reverse(); } else { /*Start animation*/ _animationController!.forward(); } } }, ), ); } @override void dispose() { _animationController!.dispose(); // TODO: implement dispose super.dispose(); } }
- Attention
- Whenever you create an animation, you need to create an AnimationController object, which is a cornerstone of using animation
- If you want to use the curvedanimationed object, the value of the AnimationController must be a value between 0 and 1, or an error will be reported
2.2 AnimatedWidget
- There are several disadvantages in implementing animation in the above way. In the above code, we must monitor the change of animation value, and call setState after the change, which will bring two problems:
- The execution animation must include this part of code, which is redundant
- Calling setState means that the build method in the entire State class will be rebuilt
How can the above operations be optimized? AnimatedWidget
Create a Widget that inherits from the AnimatedWidget:
class GYAnimationIcon extends AnimatedWidget { GYAnimationIcon(Animation sizeAimation) : super(listenable: sizeAimation); @override Widget build(BuildContext context) { Animation anim = listenable as Animation; // TODO: implement build return Icon(Icons.favorite, size: anim.value,color: Colors.red,); } }
Modification_ Code in GYHomePageState
class _GYHomePageState extends State<GYHomePage> with SingleTickerProviderStateMixin { //Create AnimationController AnimationController? _animationController; //Must be initialized or an optional type defined CurvedAnimation? _curvedAnimation; Animation? _sizeAnimation; @override void initState() { // TODO: implement initState super.initState(); //1. Create AnimationController _animationController = AnimationController(vsync: this, duration: Duration(seconds: 2)); //2. Set the value of Curved (for example, uniform speed, some effects) _curvedAnimation = CurvedAnimation(parent: _animationController!, curve: Curves.linear); //3. Set Tween (function: change the size of animation value, because by default, the animation value of AnimationController is [0.0, 1.0]. If you want to use other values, you must use Tween) // The type of the two values of Tween(begin: 50, end: 150) is T, that is, generic, but actually we need to be double. These write 50 are Int by default, so some 50.0 is required here _sizeAnimation = Tween(begin: 50.0, end: 150.0).animate(_curvedAnimation!); //Associate values with animation //Monitor animation execution status _animationController!.addStatusListener((status) { if (status == AnimationStatus.completed) { _animationController!.reverse(); } else if (status == AnimationStatus.dismissed) { _animationController!.forward(); } }); } @override Widget build(BuildContext context) { // TODO: implement build print("_GYHomePageState-===========build Method execution"); return Scaffold( appBar: AppBar(title: Text("home page"),), body: Center( child: GYAnimationIcon(_sizeAnimation!), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_circle_outline), onPressed: () { print("Play animation button click event======="); if (_animationController!.isAnimating) { _animationController!.stop(); } else { if (_animationController!.status == AnimationStatus.forward) { /*Start animation*/ _animationController!.forward(); } else if (_animationController!.status == AnimationStatus.reverse) { /*Start animation*/ _animationController!.reverse(); } else { /*Start animation*/ _animationController!.forward(); } } }, ), ); } @override void dispose() { _animationController!.dispose(); // TODO: implement dispose super.dispose(); } }
2.3 AnimatedBuilder
However, the above code using AnimatedWidget is not the final solution. Because the above method also has the following two disadvantages:
- Each time we create a new class to inherit from AnimatedWidge
- If our animation Widget has child widgets, it means that its child widgets will also be rebuilt
Use AnimatedBuilder to optimize the above operations_ GYHomePageState code modification
class _GYHomePageState extends State<GYHomePage> with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { // TODO: implement build print("_GYHomePageState-===========build Method execution"); return Scaffold( appBar: AppBar(title: Text("home page"),), body: Center( child: AnimatedBuilder( animation: _animationController!, builder: (ctx, child) { return Icon(Icons.favorite, size: _sizeAnimation!.value,color: Colors.red,); }, ), ), ); } }
3. Other animation supplements
3.1 interleaving animation
Case description:
- Click the floating action button to execute the animation
- Animation combines size change, color change, rotation animation and so on
- Here, we generate multiple Animation objects through multiple tweens
class _GYHomePageState extends State<GYHomePage> with SingleTickerProviderStateMixin { //Create AnimationController AnimationController? _animationController; //Must be initialized or an optional type defined CurvedAnimation? _curvedAnimation; Animation<double>? _sizeAnimation; //Size animation Animation? _colorAnimation; //Color animation Animation? _rationAnimation; //Rotate animation @override void initState() { // TODO: implement initState super.initState(); //1. Create AnimationController _animationController = AnimationController(vsync: this, duration: Duration(seconds: 2)); //2. Set the value of Curved (for example, uniform speed, some effects) _curvedAnimation = CurvedAnimation(parent: _animationController!, curve: Curves.linear); //3. Set Tween (function: change the size of animation value, because by default, the animation value of AnimationController is [0.0, 1.0]. If you want to use other values, you must use Tween) // The type of the two values of Tween(begin: 50, end: 150) is T, that is, generic, but actually we need to be double. These write 50 are Int by default, so some 50.0 is required here _sizeAnimation = Tween(begin: 50.0, end: 150.0).animate(_curvedAnimation!); //Associate values with animation // Color animation _colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(_animationController!); //Animate rotation _rationAnimation = Tween(begin: 0.0, end: 2 * pi).animate(_animationController!); //Monitor animation execution status _animationController!.addStatusListener((status) { if (status == AnimationStatus.completed) { _animationController!.reverse(); } else if (status == AnimationStatus.dismissed) { _animationController!.forward(); } }); } @override Widget build(BuildContext context) { // TODO: implement build print("_GYHomePageState-===========build Method execution"); return Scaffold( appBar: AppBar(title: Text("home page"),), body: Center( child: AnimatedBuilder( animation: _animationController!, builder: (ctx, child) { return Transform(transform: Matrix4.rotationZ(_rationAnimation!.value), child: Icon(Icons.favorite, size: _sizeAnimation!.value,color: _colorAnimation!.value,), ); }, ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_circle_outline), onPressed: () { print("Play animation button click event======="); if (_animationController!.isAnimating) { _animationController!.stop(); } else { if (_animationController!.status == AnimationStatus.forward) { /*Start animation*/ _animationController!.forward(); } else if (_animationController!.status == AnimationStatus.reverse) { /*Start animation*/ _animationController!.reverse(); } else { /*Start animation*/ _animationController!.forward(); } } }, ), ); } @override void dispose() { _animationController!.dispose(); // TODO: implement dispose super.dispose(); } }
3.2 Hero animation
Mobile terminal development will often encounter such requirements:
- Click a avatar to display the large picture of the avatar, and from the Rect of the original image to the Rect of the large picture
- Click a picture of a commodity to display the large picture of the commodity, and from the Rect of the original image to the Rect of the large picture
This cross page shared animation is called Shared Element Transition
In fluent, there is a special Widget to achieve this animation effect: Hero
To realize Hero animation, the following steps are required:
- In the first Page1, define a starting Hero Widget, called source hero, and bind a tag;
- In the second Page2, define a Hero Widget with an endpoint, which is called destination hero, and bind the same tag
- You can jump from the first page Page1 to the second page Page2 through the Navigator
Fluent will set Tween to define the size and position of Hero from the starting point to the terminal, and perform animation effects on the layer
body: Center( child: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 16/9 ), children: List.generate(20, (index) { final imageURL = "https://picsum.photos/500/500?random=$index"; return GestureDetector( onTap: () { Navigator.of(context).push(PageRouteBuilder( pageBuilder: (ctx, anim1, anim2) { return FadeTransition(opacity: anim1, child: GYImageDetailesPage(imageURL)); } )); }, child: Hero( tag: imageURL, child: Image.network( imageURL, fit: BoxFit.cover, ), ) ); }), ), ),
class GYImageDetailesPage extends StatelessWidget { final String _imageURL; GYImageDetailesPage(this._imageURL); @override Widget build(BuildContext context) { // TODO: implement build return Scaffold( backgroundColor: Colors.black, body: Center( child: GestureDetector( onTap: () { Navigator.of(context).pop(); }, child: Hero(tag: _imageURL, child: Image.network(_imageURL)) ), ), ); } }
3.2 jump page animation
Jump method of modal test in IOS:
// IOS - > modal mode Navigator.of(context).push(MaterialPageRoute( builder: (ctx) { return GYModelPage(); }, fullscreenDialog: true ));
Transition animation gradient:
//Transition animation jump mode Navigator.of(context).push(PageRouteBuilder( transitionDuration: Duration(seconds: 3), pageBuilder: (ctx, animation1, animation2) { return FadeTransition( opacity: animation1, child: GYModelPage(), ); } ));