Words at the beginning of the animation

Animation has always been an unavoidable part of front-end programming. It’s not as essential as basic components, network request components, etc., but without animation in development, our program probably wouldn’t be a great game. Yes, animation is an integral part of making your program good.

General animation is divided into several types – frame-by-frame animation, tween animation, physics-based animation, etc.

Frame-by-frame animation is usually the continuous playback of several moments of an action, that is, a series of coherent pictures are rendered on the screen one by one in a certain sequence within a certain period of time, resulting in a visual effect of continuous action, somewhat similar to the playback of movies. The advantages of this kind of animation is to realize the complex animations easier, taking the design of the continuous good as long as the image can be achieved almost all of the animation, but its shortcomings are obvious, such as more pictures after causing software package is larger, high memory usage, cut figure number can lead to complex animation resources occupy huge, flexibility, not higher.

The word “tween” in tween animation is short for “in between”. 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. 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. Physics-based animation is an enhancement of a series of data calculations on the basis of tween animation.

Before looking at these animations in detail, let’s take a look at some of the important classes of Flutter related to animation.

  • Tween
  • Animation
  • CurvedAnimation
  • AnimationController
  • Hero

Let’s talk about animation in code

The concept of Flutter is very similar to Android property animation. They do not operate directly on graphics objects, but rather abstract the animation into a digital concept. Control of graphics objects is partially realized by the user himself. To put it simply, in the process of animation, the animation core library will give us a string of numbers. According to this string of numbers, we can determine the displacement, transparency, size and other graphics attributes of the graphics object through calculation.

  1. Tween

    Tween is derived from the Animatable class. Regardless of its parent, Tween has two member variables, begin and end, which are of type generic-constrained Dynamic. Tween’s constructor assigns values to both variables. It also has two member methods:

    /// Returns the value this variable has at the given animation clock value.
    ///
    /// The default implementation of this method uses the [+], [-], and [*]
    /// operators on `T`. The [begin] and [end] properties must therefore be
    /// non-null by the time this method is called.
    @protected
    T lerp(double t) {
      assert(begin ! =null);
      assert(end ! =null);
      return begin + (end - begin) * t as T;
    }
    
    /// Returns the interpolated value for the current value of the given animation.
    ///
    /// This method returns `begin` and `end` when the animation values are 0.0 or
    /// 1.0, respectively.
    ///
    /// This function is implemented by deferring to [lerp]. Subclasses that want
    /// to provide custom behavior should override [lerp], not [transform] (nor
    /// [evaluate]).
    ///
    /// See the constructor for details about whether the [begin] and [end]
    /// properties may be null when this is called. It varies from subclass to
    /// subclass.
    @override
    T transform(double t) {
      if (t == 0.0)
        return begin;
      if (t == 1.0)
        return end;
      return lerp(t);
    }
    Copy the code

    The transform() method returns the value of a generic constraint based on the passed double parameter. The value is generated by the lerp() method, which outputs the corresponding value at that progress. So it’s essentially a way to get an intermediate value. We might as well mark the incoming value as progress, representing the progress of the animation, and the returned value as an attribute, representing a corresponding attribute under that progress.

    So let’s look at Tween’s parent class. Three methods are self-implemented in the Animatable class.

    • T evaluate(Animation<double> animation) => transform(animation.value);
      Copy the code

      The intermediate state value is obtained from the Animation’s value (type double) member.

    • Animation<T> animate(Animation<double> parent) {
        return _AnimatedEvaluation<T>(parent, this);
      }
      Copy the code

      It returns an object, so let’s see.

      class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> {
        _AnimatedEvaluation(this.parent, this._evaluatable);
      
        @override
        final Animation<double> parent;
      
        final Animatable<T> _evaluatable;
      
        @override
        T get value => _evaluatable.evaluate(parent);
      
        @override
        String toString() {
          return '$parent\u27A9$_evaluatable\u27A9$value';
        }
      
        @override
        String toStringDetails() {
          return 'The ${super.toStringDetails()} $_evaluatable'; }}Copy the code

      Parent is also an Animation object with two member variables, parent and _evaluatable. After animate(), parent is an Animation object. The _evaluatable object is passed in by the Animatable itself, through which the properties of the Animatable can be obtained, exposed through the getter value.

      To summarize, we revisit the animate() method. It inputs an Animation

      that stores progress and outputs an Animation

      that records the properties of the Animation.

    • Animatable<T> chain(Animatable<double> parent) {
        return _ChainedEvaluation<T>(parent, this);
      }
      Copy the code

      The input is an Animatable

      object, and the return value of Animatable is an Animatable

      in the constructor of _chaineDeval.

      Then enter the “_chainedeval” file.

      class _ChainedEvaluation<T> extends Animatable<T> {
        _ChainedEvaluation(this._parent, this._evaluatable);
      
        final Animatable<double> _parent;
        final Animatable<T> _evaluatable;
      
        @override
        T transform(double t) {
          return _evaluatable.transform(_parent.transform(t));
        }
      
        @override
        String toString() {
          return '$_parent\u27A9$_evaluatable'; }}Copy the code

      Focus on the transform() method, which calls the Transform () method through two nested Animatable calls, implementing the aptly-named chain from progress -> value -> another value.

  2. Animation

    This is an abstract class that inherits the Listenable class and implements the ValueListenable interface.

    Again, look at its member variables first. It has two member variables, status, which is an object of class AnimationStatus, and value, which is a generic constraint inherited from the ValueListenable interface.

    AnimationStatus is an enumeration, and you can see that it records some state of the animation.

    // Examples can assume:
    // AnimationController _controller;
    
    /// The status of an animation
    enum AnimationStatus {
      /// The animation is stopped at the beginning
      dismissed,
    
      /// The animation is running from beginning to end
      forward,
    
      /// The animation is running backwards, from end to beginning
      reverse,
    
      /// The animation is stopped at the end
      completed,
    }
    Copy the code

    The Animation class stores the values associated with the Animation. Its member methods have a drive() :

    // There are too many comments
    @optionalTypeArgs
    Animation<U> drive<U>(Animatable<U> child) {
      assert(this is Animation<double>);
      return child.animate(this as Animation<double>);
    }
    Copy the code

    This method takes the Animatable described above, and implements the Animation class that returns properties from an Animation class that stores progress by calling its animate() method.

  3. CurvedAnimation

    It inherits from Animation, so its function is also to store the state and related values of the Animation. As its name suggests, it returns value an extra step in the role of the Curve class (passed in by the constructor). Curve is also an abstract class, and the member method transform is used to transform values. By calling transformInternal() method, the transform method realizes the method calls implemented in the extended subclasses of Curve class and realizes the state value conversion. SawTooth, for example, works like this:

    Its transformInternal() method is as follows:

    @override
    double transformInternal(double t) {
      t *= count;
      return t - t.truncateToDouble();
    }
    Copy the code

    So with this transformation, the animation process is no longer limited to linear, there are more combinations of ways, animation construction is more flexible.

  4. AnimationController

    The AnimationContrller class is a subclass of Animation. Its main functions are as follows:

    • Starts the animation with a specific value

    • Sets the upper and lower limits of animation values

    • Controls whether an animation is played forward or backward, or whether the animation is stopped

    • Create a Fling animation from a physical simulation

    The AnimationController constructor needs to pass in a TickerProvider vsync parameter. The main responsibility of the TickerProvider is to create another Ticker class.

    A SchedulerBinding is used to add callbacks to every screen refresh. Ticker uses a SchedulerBinding to add callbacks to screen refresh. TickerCallback is called every time the screen is refreshed. Using Ticker (instead of Timer) to drive animations prevents off-screen animations (when the animation’s UI is not on the current screen, such as when the screen is locked) from consuming unnecessary resources because the screen refresh in the Flutter notifies the bound SchedulerBinding. The Ticker is driven by SchedulerBinding, and since the screen will stop refreshing after the lock, the Ticker will no longer trigger.

    If it is in the State to create AnimationController, you can use TickerProviderStateMixin and SingleTickerProviderStateMixin, If the State class only needed a Ticker, the latter would be more efficient than the former.

    So let’s take a look at the implementation of some of the AnimationController’s features.

    AnimationController({
        double value,
        this.duration,
        this.reverseDuration,
        this.debugLabel,
        this.lowerBound = 0.0.this.upperBound = 1.0.this.animationBehavior = AnimationBehavior.normal,
        @required TickerProvider vsync,
      })
    Copy the code

    The AnimationController can set both lowerBound and upperBound values in its constructor.

    Start the forward animation with the forward({double from}) function (where the “from” is the specified animation start value). The Ticker starts running by calling _ticker.start() through the call chain “forward() => _animateToInternal() => _startSimulation()”. Each frame of the Ticker calls back to its _tick() method, in which the _value value is assigned by a time computed value, and thus the AnimationController completes the animation.

    Ok, so that’s the AnimationController’s animation-driven process, and of course the principle of reverse animation is the same. But in exploring the animation startup above, we also noticed a class called Simulation. This is a class used to dynamically simulate the motion state of objects, including distance (x()), speed (dx()) and isDone()). The animation ideas analyzed above let us know that these states should be known as long as a specific time value is given during the animation. Such as the default linear interpolation model class _InterpolationSimulation used in the AnimationController#_animateToInternal() method, the calculation of these states is based on linear scaling algorithms.

    class _InterpolationSimulation extends Simulation {
      _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale)
        : assert(_begin ! =null),
          assert(_end ! =null),
          assert(duration ! =null && duration.inMicroseconds > 0),
          _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
    
      final double _durationInSeconds;
      final double _begin;
      final double _end;
      final Curve _curve;
    
      @override
      double x(double timeInSeconds) {
        final double t = (timeInSeconds / _durationInSeconds).clamp(0.0.1.0) as double;
        if (t == 0.0)
          return _begin;
        else if (t == 1.0)
          return _end;
        else
          return _begin + (_end - _begin) * _curve.transform(t);
      }
    
      @override
      double dx(double timeInSeconds) {
        final double epsilon = tolerance.time;
        return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
      }
    
      @override
      bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
    }
    Copy the code

    So if you need to make some changes to the animation model, in addition to the CurvedAnimation class mentioned above, you can also make changes here. The fling() method formally customizes the nonlinear model of the SpringSimulation class in this form.

  5. Hero

    This is an encapsulated transition animation.

    Let’s start with an example of an official interface jump. No Hero animation jump code is as follows:

    class MainScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Main Screen'),
          ),
          body: GestureDetector(
            onTap: () {
              Navigator.push(context, MaterialPageRoute(builder: (_) {
                return DetailScreen();
              }));
            },
            child: Image.network(
              'https://picsum.photos/250? image=9',),),); }}class DetailScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: GestureDetector(
            onTap: () {
              Navigator.pop(context);
            },
            child: Center(
              child: Image.network(
                'https://picsum.photos/250? image=9'(), (), (), (); }}Copy the code

    And you can see that’s how we use it. The effect is as follows:

    With the Hero animation:

    import 'package:flutter/material.dart';
    
    void main() => runApp(HeroApp());
    
    class HeroApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Transition Demo', home: MainScreen(), ); }}class MainScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Main Screen'),
          ),
          body: GestureDetector(
            child: Hero(
              tag: 'imageHero',
              child: Image.network(
                'https://picsum.photos/250? image=9',
              ),
            ),
            onTap: () {
              Navigator.push(context, MaterialPageRoute(builder: (_) {
                returnDetailScreen(); })); },),); }}class DetailScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: GestureDetector(
            child: Center(
              child: Hero(
                tag: 'imageHero',
                child: Image.network(
                  'https://picsum.photos/250? image=9', ), ), ), onTap: () { Navigator.pop(context); },),); }}Copy the code

    Effect:

    I can see an obvious animation effect (ideally smoother because my device is running more slowly), as if the images in the app were straddling the two screens.

    This whole visually complete process can actually be divided into three steps:

    • In the Overlay layer, create a copy of the source Hero control with the exact same size, shape, and relative position on the screen. When the page is about to jump, push the source Hero to run hide in the background and only show a copy of the Overlay layer.

    • The page begins to jump to the target page, and the Hero element of the Overlay layer is also tweaked to the location of the target Hero and the size and shape of the target Hero. The transition animation is done by using the Hero createRectTween property to control the boundaries of the Hero element.

    • When the Hero copy of the Overlay reaches the location of the target Hero and transforms to the size and shape of the target Hero, the animation is complete. 1. Destroy the Hero pair of the Overlay layer. 2. Display the target Hero in the corresponding position on the new interface. 3. The source Hero is stored in the original page.

Let the animation fly for a while

More talk is useless, let’s write a small demo according to the above theory.

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:test_flutter/main.dart';

class AnimationSample extends StatefulWidget {
  @override
  State<AnimationSample> createState() => _AnimationSampleState();

}

class _AnimationSampleState extends State<AnimationSample> with SingleTickerProviderStateMixin {

  Tween<double> _tween = Tween<double>(begin: 0.0, end: 128.0);

  Animation<double> _animation;

  AnimationController _animationController;

  double _borderLength = 0.0;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(duration: Duration(seconds: 5), vsync: this);

    _animation = _tween.animate(_animationController);

    _animationController.addListener(() {
      setState(() {
        _borderLength = _animation.value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          width: _borderLength,
          height: _borderLength,
          child: Block(' '), ),subclass ), floatingActionButton: FloatingActionButton( onPressed: () { _animationController.forward(); }, child: Icon(Icons.play_arrow), ), ); }}Copy the code

The code here is simple: dynamically change the size of a color block. The effect is as follows:

The Tween#animate() method returns an Animation object, which is a subclass of _animatedeval. The value getter of this Animation class gets the value of the AnimationController into the value of the Tween using the transform() method. Since the values of the AnimationController default from 0.0 to 1.0, turn to the Tween section above where the transform() method is implemented (the argument t is the value of the AnimationController). As you can see, the transformation is perfect. When the AnimationController# Forward () method is executed, the value of the AnimationController is reassigned every frame (default is between 0.0 and 1.0), The ChangeNotifier mechanism notifes all value listeners of updates, so Tween values are refreshed frame by frame.

Let’s change the code slightly:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:test_flutter/main.dart';

class AnimationSample extends StatefulWidget {
  @override
  State<AnimationSample> createState() => _AnimationSampleState();

}

class _AnimationSampleState extends State<AnimationSample> with SingleTickerProviderStateMixin {

  AnimationController _animationController;

  double _borderLength = 0.0;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
        duration: Duration(seconds: 5),
        vsync: this,
        lowerBound: 0.0,
        upperBound: 128.0);

    _animationController.addListener(() {

      setState(() {
        _borderLength = _animationController.value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          width: _borderLength,
          height: _borderLength,
          child: Block(' '), ), ), floatingActionButton: FloatingActionButton( onPressed: () { _animationController.forward(); }, child: Icon(Icons.play_arrow), ), ); }}Copy the code

We removed the Tween part, so we only animate the value of the AnimationController and then listen on it, so we can get the value of each frame as well. Run the code, the effect is the same as above.

conclusion

Ok, animation things so much, realize the separation of logic layer and implementation layer idea, the logical layer is carefully designed, graphics related implementation layer things to the developer, not only can reduce coupling, improve abstraction, but also can meet different needs and flexible needs. After a peek at the mysteries of animation, all you need to do is make these cute animations work in your application.