For a front-end App, adding appropriate animation can give users better experience and visual effects. So whether it’s native iOS or Android, or front-end development, there are apis for doing some animations.

Flutter has its own rendering loop, and we can certainly provide it with a data model to help us animate it.

I. Understanding of animation API

Animation means that we provide different values to the Flutter engine in some way (such as objects, Animation objects), and the Flutter can add smooth Animation effects to the corresponding Widget based on the values we provide.

In this chapter on animation, I’m going to explain the relationship between the apis and what they do, and then explain how to use these apis to achieve different animation effects.

1.1. Animation

In Flutter, the core class that implements animations is the Animation. Widgets can directly incorporate these animations into their build methods to read their current values or listen for their state changes.

Let’s take a look at Animation, which is an abstract class:

  • AddListener method
    • The animation notifies all listeners added through addListener whenever the animation’s state value changes.
    • Usually, one is listening for animationstateObject will call itselfsetStateMethod to inform the widget system that it needs to be rebuilt based on the new state value by passing itself to these listener callback functions.
  • addStatusListener
    • All listeners added via addStatusListener are notified when the state of the animation changes.
    • Normally, the animation will start fromdismissedThe start of the state indicates that it is at the beginning of the change interval.
    • For example, animations from 0.0 to 1.0 indismissedThe value should be 0.0 in state.
    • The next state of the animation might beforward(say 0.0 to 1.0) orreverse(e.g. 1.0 to 0.0).
    • Finally, if the animation reaches the end point of its interval (say 1.0), the animation becomescompletedState.
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
  const Animation();
  
  // Add an animation listener
  @override
 void addListener(VoidCallback listener);   // Remove the animation listener  @override  void removeListener(VoidCallback listener);   // Add an animation state listener  void addStatusListener(AnimationStatusListener listener);   // Remove the animation state listener  void removeStatusListener(AnimationStatusListener listener);   // Get the current state of the animation  AnimationStatus get status;   // Get the current value of the animation  @override  T get value; Copy the code

1.2. AnimationController

Animation is an abstract class and cannot be used to create objects directly for Animation.

AnimationController is a subclass of Animation. To animate, we usually create an AnimationController object.

  • The AnimationController generates a set of values, which by default are between 0.0 and 1.0;

In addition to listening above to get the state and value of the animation, the AnimationController also provides control over the animation:

  • Forward: Executes the animation forward
  • Reverse: Animation is played in the direction
  • Stop: Stops the animation

AnimationController AnimationController

class AnimationController extends Animation<double>
  with AnimationEagerListenerMixin.AnimationLocalListenersMixin.AnimationLocalStatusListenersMixin {
  AnimationController({
    // Initialize the value
    double value,
 // Animation execution time  this.duration,  // Reverse animation execution time  this.reverseDuration,  / / the minimum  this.lowerBound = 0.0. / / Max  this.upperBound = 1.0. // Refresh rate ticker callback (see below for details)  @required TickerProvider vsync,  }) } Copy the code

AnimationController has a mandatory parameter vsync. What is it?

  • Earlier I talked about the rendering loop of Flutter, where Flutter waits for a Vsync signal every time it renders a frame.
  • This is also to listen for vsync signals. If the application developed by Flutter no longer accepts sync signals (such as locking the screen or backing into the background), continuing to perform the animation will cost performance.
  • At this point we set up the Ticker and no longer start the animation.
  • Is more common in the development of the SingleTickerProviderStateMixin blended into the definition of the State.

1.3. CurvedAnimation

CurvedAnimation is also an implementation of Animation, which adds Animation curves to the AnimationController:

  • A CurvedAnimation can combine the 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,
    // Curve object
 @required this.curve,  this.reverseCurve,  }); } Copy the code

Curve objects have some constant Curves (like Colors) that we can use directly:

  • For the effect of the value, you can directly check the official website (there is a corresponding GIF effect, at a glance)
  • https://api.flutter.dev/flutter/animation/Curves-class.html

The official also gives an example of how to define Curse:

import 'dart:math';

class ShakeCurve extends Curve {
  @override
  double transform(double t) => sin(t * pi * 2);
} Copy the code

1.4. Tween

By default, the AnimationController animation generates values between 0.0 and 1.0

  • If you want to use a value other than that, or a data type other than that, you need to use Tween

Tween

  • The source code is very simple, just pass in two values, you can define a range.
class Tween<T extends dynamic> extends Animatable<T> {
  Tween({ this.begin, this.end });
}
Copy the code

Tween also has subclasses, such as ColorTween and BorderTween, to set the value of the animation for the animation or the border.

Tween.animate

To use a Tween object, you call Tween’s animate() method, passing in an Animation object.

2. Animation case exercises

2.1. Basic use of animation

Let’s complete a case:

  • Click on the case to perform a heartbeat animation, which can be performed repeatedly
  • Click again to pause and restart the animation
Animation effects
class HYHomePage extends StatelessWidget {
  final GlobalKey<_AnimationDemo01State> demo01Key = GlobalKey();

  @override
  Widget build(BuildContext context) {
 return Scaffold(  appBar: AppBar(  title: Text("Animation test"),  ),  body: AnimationDemo01(key: demo01Key),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.play_circle_filled),  onPressed: () {  if(! demo01Key.currentState.controller.isAnimating) { demo01Key.currentState.controller.forward();  } else {  demo01Key.currentState.controller.stop();  }  },  ),  );  } }  class AnimationDemo01 extends StatefulWidget {  AnimationDemo01({Key key}): super(key: key);   @override  _AnimationDemo01State createState() => _AnimationDemo01State(); }  class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {  AnimationController controller;  Animation<double> animation;   @override  void initState() {  super.initState();   // 1. Create AnimationController  controller = AnimationController(duration: Duration(seconds: 1), vsync: this);  // 2. Add Curve effect to animation  animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);  // 3  animation.addListener(() {  setState(() {});  });  // 4. Control the animation flip  animation.addStatusListener((status) {  if (status == AnimationStatus.completed) {  controller.reverse();  } else if (status == AnimationStatus.dismissed) {  controller.forward();  }  });  // 5. Set the value range  animation = Tween(begin: 50.0, end: 120.0).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: Icon(Icons.favorite, color: Colors.red, size: animation.value,),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } Copy the code

2.2. AnimatedWidget

In the above code, we have to listen for the animation value to change and then call setState, which causes two problems:

  • 1. Animation execution must include part of the code, which is redundant
  • 2. Calling setState means that the entire build method in the State class will be rebuilt

How can you optimize the above operation? AnimatedWidget

Create a Widget inherited from the AnimatedWidget:

class IconAnimation extends AnimatedWidget {
  IconAnimation(Animation animation): super(listenable: animation);

  @override
  Widget build(BuildContext context) {
 Animation animation = listenable;  return Icon(Icons.favorite, color: Colors.red, size: animation.value,);  } } Copy the code

Modify the code in _AnimationDemo01State:

class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
 void initState() {  super.initState();   // 1. Create AnimationController  controller = AnimationController(duration: Duration(seconds: 1), vsync: this);  // 2. Add Curve effect to animation  animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);  // 3  // 4. Control the animation flip  animation.addStatusListener((status) {  if (status == AnimationStatus.completed) {  controller.reverse();  } else if (status == AnimationStatus.dismissed) {  controller.forward();  }  });  // 5. Set the value range  animation = Tween(begin: 50.0, end: 120.0).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: IconAnimation(animation),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } Copy the code

2.3. AnimatedBuilder

Is Animated the best solution?

  • 1. Each time we create a new class to inherit from the AnimatedWidget

  • 2. If our animated Widget has child widgets, that means its child widgets will also be rebuilt

How can you optimize the above operation? AnimatedBuilder

class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
 void initState() {  super.initState();   // 1. Create AnimationController  controller = AnimationController(duration: Duration(seconds: 1), vsync: this);  // 2. Add Curve effect to animation  animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);  // 3  // 4. Control the animation flip  animation.addStatusListener((status) {  if (status == AnimationStatus.completed) {  controller.reverse();  } else if (status == AnimationStatus.dismissed) {  controller.forward();  }  });  // 5. Set the value range  animation = Tween(begin: 50.0, end: 120.0).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: AnimatedBuilder(  animation: animation,  builder: (ctx, child) {  return Icon(Icons.favorite, color: Colors.red, size: animation.value,);  },  ),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } Copy the code

3. Other animation supplement

3.1. Interweaving animation

Case description:

  • Click on floatingActionButton to animate
  • Animation set transparency change, size change, color change, rotation animation, etc.;
  • We’re generating multiple Animation objects with multiple Tween’s;
Mixed animation
import 'dart:math';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());
  class MyApp extends StatelessWidget {  // This widget is the root of your application.  @override  Widget build(BuildContext context) {  return MaterialApp(  title: 'Flutter Demo'. theme: ThemeData(  primarySwatch: Colors.blue, splashColor: Colors.transparent),  home: HYHomePage(),  );  } }  class HYHomePage extends StatelessWidget {  final GlobalKey<_AnimationDemo01State> demo01Key = GlobalKey();   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("List test"),  ),  body: AnimationDemo01(key: demo01Key),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.play_circle_filled),  onPressed: () {  demo01Key.currentState.controller.forward();  },  ),  );  } }  class AnimationDemo01 extends StatefulWidget {  AnimationDemo01({Key key}): super(key: key);   @override  _AnimationDemo01State createState() => _AnimationDemo01State(); }  class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {  AnimationController controller;  Animation<double> animation;   Animation<Color> colorAnim;  Animation<double> sizeAnim;  Animation<double> rotationAnim;   @override  void initState() {  super.initState();   // 1. Create AnimationController  controller = AnimationController(duration: Duration(seconds: 2), vsync: this);  // 2. Add Curve effect to animation  animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);  // 3  animation.addListener(() {  setState(() {});  });   // 4. Change the value  colorAnim = ColorTween(begin: Colors.blue, end: Colors.red).animate(controller);  sizeAnim = Tween(begin: 0.0, end: 200.0).animate(controller);  rotationAnim = Tween(begin: 0.0, end: 2*pi).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: Opacity(  opacity: animation.value,  child: Transform(  alignment: Alignment.center,  transform: Matrix4.rotationZ(animation.value),  child: Container(  width: sizeAnim.value,  height: sizeAnim.value,  color: colorAnim.value,  alignment: Alignment.center,  ),  ),  ),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } Copy the code

Of course, we can use Builder to optimize the code

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: controller,
 builder: (ctx, child) {  return Opacity(  opacity: animation.value,  child: Transform(  alignment: Alignment.center,  transform: Matrix4.rotationZ(rotationAnim.value),  child: Container(  width: sizeAnim.value,  height: sizeAnim.value,  color: colorAnim.value,  alignment: Alignment.center,  ),  ),  );  },  ),  );  } Copy the code

3.2. Hero animation

Mobile development often encounters requirements like this:

  • Click on an avatar to display a larger image of the avatar and go from the Rect of the original image to the Rect of the larger image
  • Click on an image of an item to display a larger image of the item and move from the Rect of the original image to the Rect of the larger image

This kind of animation Shared across pages is called Shared Element Transition.

There is a special Widget in Flutter to achieve this animation effect: Hero

To implement the Hero animation, the following steps are required:

  • 1. In the first Page1, define a starting Hero Widget, called the Source Hero, and bind it with a tag;
  • 2. In the second Page2, define an end Hero Widget, called destination Hero, and bind the same tag;
  • 3. You can use Navigator to realize the jump from the first page Page1 to the second page Page2;

The Flutter sets the Tween to define the size and position of the Hero from the start to the end and performs animation on the layer.

Home Page code:

Home Page
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:testflutter001/animation/image_detail.dart';

void main() => runApp(MyApp());  class MyApp extends StatelessWidget {  // This widget is the root of your application.  @override  Widget build(BuildContext context) {  return MaterialApp(  title: 'Flutter Demo'. theme: ThemeData(  primarySwatch: Colors.blue, splashColor: Colors.transparent),  home: HYHomePage(),  );  } }  class HYHomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("Hero animation"),  ),  body: HYHomeContent(),  );  } }  class HYHomeContent extends StatelessWidget {  @override  Widget build(BuildContext context) {  return GridView(  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(  crossAxisCount: 2. crossAxisSpacing: 8. mainAxisSpacing: 8. childAspectRatio: 2  ),  children: List.generate(20, (index) {  String imageURL = "https://picsum.photos/id/$index/ 400/200";  return GestureDetector(  onTap: () {  Navigator.of(context).push(PageRouteBuilder(  pageBuilder: (ctx, animation, animation2) {  return FadeTransition(  opacity: animation,  child: HYImageDetail(imageURL),  );  }  ));  },  child: Hero(  tag: imageURL,  child: Image.network(imageURL)  ),  );  }),  );  } } Copy the code

Picture display Page

Picture display Page
import 'package:flutter/material.dart';

class HYImageDetail extends StatelessWidget {
  final String imageURL;

 HYImageDetail(this.imageURL);   @override  Widget build(BuildContext context) {  return Scaffold(  backgroundColor: Colors.black,  body: Center(  child: GestureDetector(  onTap: () {  Navigator.of(context).pop();  },  child: Hero(  tag: imageURL,  child: Image.network(  this.imageURL,  width: double.infinity,  fit: BoxFit.cover,  ),  )),  ),  );  } } Copy the code

Note: All content will be published on our official website. Later, Flutter will also update other technical articles, including TypeScript, React, Node, Uniapp, MPvue, data structures and algorithms, etc. We will also update some of our own learning experiences

The public,