This is the 11th day of my participation in Gwen Challenge

Flutter is a new cross-platform, open source UI framework developed by Google that supports iOS and Android. Flutter is the default development kit for Fuchsia, the future operating system. It is also the most popular cross-platform solution.

preface

Well-designed animations can improve the user experience by making the user interface feel more intuitive and smooth. Flutter animation support makes it easy to implement a variety of animation types. Many widgets, especially Material Design Widgets, come with standard animation effects defined in their Design specification, but you can customize these effects as well.

First, the importance of animation

Well-designed animations can improve the user experience by making the user interface feel more intuitive and smooth. Flutter animation support makes it easy to implement a variety of animation types. Many widgets, especially Material Design Widgets, come with standard animation effects defined in their Design specification, but you can customize these effects as well.

Animation on all platforms is basically the same principle, is in a period of time in a series of continuous changes in the frame composition of the picture. In Flutter, the animation process is quantified into a set of values that can be used to animate the properties of the control. Within Flutter, there are four key parts to animate the process.

1.1 Interpolation (Tweens)

Tweens provides starting and ending values for the animation. By default, the animation in a Flutter maps the value at any given moment to a double value between 0.0 and 1.0. We can define the value range from -200.0 to 0.0 using the following Tween:

tween = Tween<double>(begin: - 200., end: 0);
Copy the code

We can also set the value to the object that needs to be changed accordingly, for example by setting the starting value to red and the ending value to blue, tweens will generate an animation that gradually changes from red to blue. As follows:

colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
Copy the code

1.2 Animation Curves

Curves adjusts the rate of change over time during animation, and by default, the animation changes in a uniform linear model. The Curves class allows you to define the rate of change of the animation, such as accelerating, slowing down, or accelerating before slowing down. The Flutter also provides a series of Curves objects that achieve the corresponding rate of change:

  • linear

  • decelerate

  • ease

  • easeIn

  • easeOut

  • easeInOut

  • fastOutSlowIn

  • bounceIn

  • bounceOut

  • bounceInOut

  • elasticIn

  • elasticOut

  • elasticInOut

1.3 Ticker will

The animation in Flutter is achieved with frequent redrawing of the screen, i.e., 60 frames per second. The Ticker can be applied to every Flutter object. When the object implements the Ticker function, it will be notified of every animation frame change. There is no need for developers to manually implement Tickers for objects. Flutter provides the TickerProvider class to help quickly implement this feature. For example, when using animation under stateful controls, you usually want to mix in TickerProviderStateMixin under the State object.

class _MyAnimationState extends State<MyAnimation> 
    with TickerProviderStateMixin {}Copy the code

1.4 AnimationController (AnimationController)

The animation implementation of Flutter also has a very important class called AnimationController, the AnimationController. Obviously, we use it to control animation, i.e. animation start, pause, etc. It takes two parameters, the first is vsync, a Ticker object that notifies the object when it receives new values from Tweens and Curves, and the second duration parameter is how long the animation lasts.

/ / with SingleTickerProviderStateMixin for Ticker of object function
class _AnimatedContainerState extends State<AnimatedContainer>
        with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // Create the AnimationController animation
    _controller = AnimationController(
      // Pass in the Ticker object
      vsync: this.// Pass in the animation duration
      duration: new Duration(milliseconds: 1000)); startAnimation(); } Future<void> startAnimation() async {
    // Call the forward method of AnimationController to start the animation
    await _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _controller.value;
      child: / /...); }}Copy the code

The AnimationController inherits from the Animation and has a set of methods for controlling the Animation, such as starting the Animation with the forward() method, repeating the Animation with the repeat() method, and obtaining the current value through its value property.

Second, the type of animation

There are two types of animation in Flutter: Tween based or P2 physics-based.

2.1 Tween animation

In tween animation, start and end points, time lines, and curves that define transformation time and speed are defined. The framework then calculates how to transition from the start point to the end point.

2.2 Physics-based animation

In physics-based animation, motion is simulated to resemble real-world behavior. For example, when you throw a ball, where it lands depends on how fast it is thrown, how heavy it is, and how far it is from the ground. Similarly, a ball attached to a spring is dropped (and bounced) in a different way than a ball attached to a string.

3. Flutter Animation Library

Animation: is a core class in the Flutter Animation library. It generates values that guide the Animation.

CurvedAnimation: A subclass of Animation, abstracting the process as a nonlinear curve;

AnimationController: a subclass of Animation to manage the Animation;

Tween: Generates values between the data ranges used by the object being animated. For example, Tween can generate color values from red to blue, or from 0 to 255;

3.1 Animation

In Flutter, the Animation object itself has nothing to do with the UI rendering. Animation is an abstract class that has its current value and state (completed or stopped). One of the more commonly used Animation classes is Animation. The Animation object in a Flutter is a class that generates values between intervals in turn over a period of time. The output of an Animation object can be linear, curvilinear, a stepping function, or any other mapping that can be designed. Depending on how the Animation object is controlled, the Animation can run in reverse, or even switch directions in between.

  • Animation can also generate values of other types than double, such as Animation or Animation;

  • The Animation object has state. You can get the current value of the animation by accessing its value property;

  • The Animation object itself has nothing to do with UI rendering;

3.2 CurvedAnimation

CurvedAnimation defines the animation process as a nonlinear curve.

final CurvedAnimation curve =
   newCurvedAnimation(parent: controller, curve: Curves.easeIn); The Curves class defines many common Curves, and you can create your own, for example:class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2); }}Copy the code

3.3 AnimationController

The AnimationController is a special Animation object that generates a new value every frame that the screen refreshes. By default, the AnimationController generates linear numbers from 0.0 to 1.0 over a given period of time. For example, the following code creates an Animation object:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);
Copy the code

The AnimationController is derived from the Animation, so it can be used anywhere an Animation object is needed. However, the AnimationController has other ways of controlling animations:

  • Forward () : Start animation;

  • Reverse ({double from}) : plays the animation backwards;

  • Reset () : Resets the animation to the beginning of the animation;

  • Stop ({bool canceled = true}) : Stops the animation;

When an AnimationController is created, a vsync parameter needs to be passed. The existence of vsync prevents the off-screen animation from consuming unnecessary resources. You can use a stsync value as a vsync value.

In some cases, the value (position, the current value of the value animation) may be outside the 0.0-1.0 range of the AnimationController. For example, the Fling () function allows you to provide velocity, force, position(through the Force object). A position can be anything, so it can be out of the 0.0 to 1.0 range. CurvedAnimation can also generate values outside the 0.0 to 1.0 range. Depending on the curve chosen, the output of CurvedAnimation can have a larger range than the input. For example, elastic Curves such as Curves. ElasticIn generate values greater than or less than the default range.

3.4 Tween

By default, the AnimationController object ranges from 0.0 to 1.0. If you need different ranges or different data types, you can use Tween to configure the animation to generate values for different ranges or data types. For example, in the following example, Tween generates values from -200.0 to 0.0:

final Tween doubleTween = new Tween<double>(begin: 200.0, end: 0.0);
Copy the code

Tween is a stateless object that requires begin and end values. Tween’s sole responsibility is to define the mapping from the input range to the output range. The input range is usually 0.0 to 1.0, but this is not required.

Tween inherits from Animatable, not Animation. Animatable is similar to Animation in that it does not have to output a double. For example, ColorTween specifies the transition between two colors.

final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);
Copy the code

The Tween object does not store any state. Instead, it provides the Evaluate (Animationanimation) method to apply the mapping function to the current value of the animation. The current value of the Animation object can be obtained using the value() method. The evaluate function also performs some other processing, such as ensuring that the start and end states are returned at 0.0 and 1.0, respectively. To use a Tween object, you call its animate() method, passing in a controller object. For example, the following code generates integer values from 0 to 255 in 500 milliseconds.

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
// Note that animate() returns an Animation, not an Animatable.
Copy the code

The following example builds a controller, a curve, and a Tween:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
Copy the code

Add animations to widgets

In the example below we added a small zoom animation for a logo:

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  AnimationStatus animationState;
  double animationValue;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // #docregion addListener
    animation = Tween<double>(begin: 0, end: 300).animate(controller) .. addListener(() {// #enddocregion addListener
        setState(() {
          animationValue = animation.value;
        });
        // #docregion addListener
      })
      ..addStatusListener((AnimationStatus state) {
        setState(() {
          animationState = state;
        });
      });
    // #enddocregion addListener
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 50),
      child: Column(
        children: <Widget>[
          GestureDetector(
            onTap: () {
              controller.reset();
              controller.forward();
            },
            child: Text('Start', textDirection: TextDirection.ltr),
          ),
          Text('State:' + animationState.toString(),
              textDirection: TextDirection.ltr),
          Text('Value:' + animationValue.toString(),
              textDirection: TextDirection.ltr),
          Container(
            height: animation.value,
            width: animation.value,
            child: FlutterLogo(),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose(); }}Copy the code

Add a listener to the animation

Sometimes we need to know the progress and state of Animation execution. In Flutter, we can add listeners to Animation using addListener and addStatusListener methods: addListener: is called when the value of the Animation changes;

AddStatusListener: called when animation state changes;@override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      // #enddocregion print-state
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if(status == AnimationStatus.dismissed) { controller.forward(); }})// #docregion print-state
      ..addStatusListener((state) => print('$state'));
      ..addListener(() {
        // #enddocregion addListener
        setState(() {
          // The state that has changed here is the animation object’s value.
        });
        // #docregion addListener
      });
    controller.forward();
  }
Copy the code

Six, AnimatedWidget

We can think of the AnimatedWidget as the assistant of Animation, which can simplify the use of Animation. In the study of adding Animation to the widget, we can easily find that The AnimatedWidget simplifies seeing the animation by manually calling the animation’s addListener() and adding setState in the callback without using it. In the following refactoring example, LogoApp now inherits from the AnimatedWidget instead of the StatefulWidget. The AnimatedWidget uses the current value of the animation when drawing. LogoApp still manages the AnimationController and Tween.

class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Container(
        margin: new EdgeInsets.symmetric(vertical: 10.0),
        height: animation.value,
        width: animation.value,
        child: newFlutterLogo(), ), ); }}class LogoApp extends StatefulWidget {
  _LogoAppState createState() => new _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose(); }}Copy the code

6.1 AnimatedBuilder

AnimatedBuilder is a generic widget for building animations. AnimatedBuilder is useful for more complex widgets that you want to include animations as part of a larger build function. AnimatedBuilder is a utility class that separates animations from widgets: One problem with our code in the example above is that changing the animation requires changing the widget that displays the logo. A better solution is to separate responsibilities:

  • According to the logo

  • Defining an Animation object

  • Render transitions

Next we use the AnimatedBuilder]() class to accomplish this separation. AnimatedBuilder is a separate class in the rendering tree. Similar to AnimatedWidget, AnimatedBuilder automatically listens for notifications from an Animation object. There is no need to manually call addListener(). We create our code based on the widget tree shown below:

.// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  Widget build(BuildContext context) => Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        child: FlutterLogo(),
      );
}
// #enddocregion LogoWidget

// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Container(
                  height: animation.value,
                  width: animation.value,
                  child: child,
                ),
            child: child),
      );
}
// #enddocregion GrowTransition

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }
  // #enddocregion print-state

  @override
  Widget build(BuildContext context) => GrowTransition(
        child: LogoWidget(),
        animation: animation,
      );

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  // #docregion print-state
}
Copy the code

Flutter animation class source

7.1 Animation

Animation is an abstract class that stores the state and current value of an Animation. The most commonly used Animation class is Animation. There are many types of T. The value of the current Animation can be obtained through the value property of the Animation. Monitor for animation:

  • AddListener () The listener for each frame of animation execution

  • AddStatusListener () listens for changes in animation state.

AnimationController

  • The AnimationController inherits the Animation to control the execution, stopping, and so on of the Animation.
  • The AnimationController will generate a new value every frame of the animation. By default, — AnimationController generates linear numbers from 0.0 to 1.0 (the default range) over a given period of time.

To create the AnimationController, pass in a vsync parameter.

Tween

  • By default, the AnimationController object ranges from 0.0 to 1.0.

  • If you need different ranges or data types, you need to tween to configure the animation to generate values for different ranges or data types.

7.2 AnimatedWidget

abstract class AnimatedWidget extends StatefulWidget {
    // Create a widget to refactor when ListEnable changes
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable ! =null),
       super(key: key);
  // Declare a Listenable, frame animation listener
  final Listenable listenable;

  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override_AnimatedState createState() => _AnimatedState(); . }Copy the code

The AnimatedWidget inherits its state from the StatefulWidget and instantiates a ListEnable to listen for frame animation and refresh the AnimatedWidget when the animation method changes.

// the __AnimatedState method is as follows:
class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    // Add a listening callback
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }
    // Trigger a refresh when the frame animation changes
  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }
// Call the build() method to reconstruct the AnimatedWidget
  @override
  Widget build(BuildContext context) => widget.build(context);
}
Copy the code

When the Refresh is triggered by ListEnable, setState is called to refactor the AnimatedWidget, although setState is still called in the end, but the refreshed object is different.

7.3 CurvedAnimation

By default, Tween animation provides linear changes within the interval. If we need curve changes, we need to use CurvedAnimation together.

class HomePageState extends State<HomePage> with TickerProviderStateMixin{

  Animation animation;
  AnimationController controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
    CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = Tween<double>(begin: 0.0,end: 500).animate(curve) .. addListener((){ setState(() { }); }); controller.forward(); }@override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      alignment: Alignment.topCenter,
      child: Container(
        margin: EdgeInsets.only(top: animation.value),
        width: 100,
        height: 100, child: FlutterLogo(), ), ); }}// CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut); Here we use the bounceOut effect

class _BounceOutCurve extends Curve {
  const _BounceOutCurve._();

  @override
  double transformInternal(double t) {
    return_bounce(t); }}double _bounce(double t) {
  if (t < 1.0 / 2.75) {
    return 7.5625 * t * t;
  } else if (t < 2 / 2.75) {
    t -= 1.5 / 2.75;
    return 7.5625 * t * t + 0.75;
  } else if (t < 2.5 / 2.75) {
    t -= 2.25 / 2.75;
    return 7.5625 * t * t + 0.9375;
  }
  t -= 2.625 / 2.75;
  return 7.5625 * t * t + 0.984375;
}
// bounceOut inherits Curve, implements its transformInternal method, and implements its trajectory in transformInternal.
Copy the code

7.4 Use AnimatedBuilder to refactor our widget

The above demo code is mainly an example of implementing a color change. What if we now need to implement a size change widget? Do you want to extend the AnimationWidget after declaring a SizeAnimationWidget? Obviously it’s ok to do that. Better yet, use AnimatedBuilder to refactor our widget.

The AnimatedBuilder inherits from the abstract AnimationWidget in order to build a generic AnimationWidget implementation class without having to create an implementation class each time an AnimationWidget is used.

/ / AnimatedBuilder source code
class AnimatedBuilder extends AnimatedWidget {
  /// Creates an animated builder.
  const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  }) : assert(animation ! =null),
       assert(builder ! =null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;

  final Widget child;

  @override
  Widget build(BuildContext context) {
    returnbuilder(context, child); }}// We just need to pass in the animation and Builder
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  Animation animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new GrowTransition(child: new LogoWidget(), animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose(); }}class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) {
    return new Center(
      child: new AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return newContainer( height: animation.value, width: animation.value, child: child); }, child: child), ); }}class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 10.0),
      child: newFlutterLogo(), ); }}Copy the code

conclusion

Through the above, we can easily encapsulate a series of control Animation, but this implementation requires us to provide our own Animation object, and then through the provided interface method to start our Animation, control properties provided by the Animation object and change during the Animation process to achieve the effect of Animation. To make animation more convenient, Flutter helps developers implement animation effects from another perspective in a simpler way — implicit Animation Widgets. With implicit animation components, there is no need to manually implement interpolators, curves, etc. Developers do not even need to use AnimationController to start the animation. It is implemented more closely to the operation of the component itself. We can directly change the properties of the implicit animation component through setState(). It internally implements the transition effect of the animation process for us, that is, hides all the animation implementation details