Fluent learning - Animation


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:

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(),
                 );
               }
           ));

Tags: iOS Flutter

Posted on Fri, 19 Nov 2021 10:00:05 -0500 by Kold