Introduction to the

In the UI framework of any system, animation is implemented in the same way: changing the appearance of the UI many times quickly over a period of time; Because the human eye will produce visual pause, so the end is a continuous animation;

We call each UI change an animation frame, which corresponds to a screen refresh, and one of the most important indicators of animation smoothness is the FPS, or the number of animation frames per second. The higher the frame count, the smoother the animation will be;

Generally, if the frame rate of animation exceeds 16FPS, it will be smooth, and if the frame rate exceeds 32FPS, it will be very delicate and smooth. However, if the frame rate exceeds 32FPS, the human eye will basically not feel the difference. Since every frame of animation needs to change the UI output, it has high requirements on hardware and software of the device, so in the UI system, The average number of frames of animation is an important metric, and ideally, Flutter can achieve 60FPS, which is about the same as native apps

Flutter animation abstraction

To facilitate Animation creation, animations are abstracted by different UI systems. For example, Android can describe an Animation using XML and set it to a View. Animation, Curve, Controller, etc., are abstracted by Flutter. Tween these four roles, they work together to complete a complete animation.

Animation

Animation is an abstract class, which itself has nothing to do with UI rendering. Its main function is to save the interpolation and state of Animation, among which Animation is commonly used. An Animation is a Tween that is generated over a period of time.

The output of an Animation object can be linear, curved, a step function, or a Curve function, depending on Curve. Depending on how the Animation object is controlled, the Animation can move forward, backward, or switch directions in between. Animation can also generate Animation, or Animation, etc. In each frame of the Animation, we can obtain the current value of the Animation by using the value property of the Animation object.

The Animation in Flutter is based on an Animation object. The widget can read the Animation object’s current value in the build function and listen for changes in the Animation’s state

Animation perception

We can use Animation to monitor every frame of the Animation and the change of the execution state. Nimation can be written as follows:

AddListener () adds a frame listener to the Animation that will be called every frame. The most common behavior in frame listeners is to call setState after a state change to trigger a UI rebuild

2, addStateListener, add animation state change listener, can listen to the animation start, end, forward, direction, etc., trigger will call change listener

Curved

The process of animation can be uniform, accelerated, first plus then minus, etc. Curve is used to describe the animation process in Flutter. We call uniform animation (Curves. Linear) and non-uniform animation (nonlinear).

We can specify the curve of the animation by using a CurvedAnimation, for example:

final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeIn);
Copy the code

CurvedAnimation inherits from the Animation class

CurvedAnimation can generate a new animation object by wrapping the AnimationController and Curve. This is how we formally associate the animation with the Curve that the animation executes.

EaseIn is a preset enumeration class that defines a number of commonly used Curves, as follows:

Curves curve The animation process
linear uniform
decelerate Uniformly retarded
ease Slow down at first, then accelerate
easeIn From slow to fast
easeOut From fast to slow
easeInOut Slow at first, then speed up, then slow down again

In addition to the Curves listed above, the Curves have a lot of other Curves that you can see with animations

Of course, we can also create our own Curve, such as defining a sinusoidal Curve:

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2); }}Copy the code

AnimationController

The AnimationController is used to control the animation. It contains methods like forward(start), Stop (stop), reverse(reverse), etc. The AnimationController will generate a new value for each frame of the animation. The default range given by default is 0.0 to 1.0. For example, the following code creates an Animaction object:

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

The number range generated by the AnimationController can be specified with lowerBound and upperBound, for example:

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

AnimationControler derives the child Animation and can therefore be used wherever the Animation object is needed. But it has other methods to control animation, such as start forward animation, reverse animation and so on. Animation frames are generated after animation execution, and each refresh of the screen is an animation frame;

In each frame of the Animation, the current animation.value is generated along with the Animation curve. Then build the UI according to the current animation value. When all animation frames are triggered in sequence, the animation value will change, and the corresponding UI will change. Finally, you can see the complete animation.

Duration indicates how long an animation should be executed, and it is used to control the speed of the animation.

Note: In some cases, the animation may go beyond the 0.0 to 1 return, depending on the specific curve. For example, fing() functions can simulate a finger swing animation based on the speed and force of the gesture slide, so its animation value can be outside the range of [0.0,1.0]. That is, depending on the selected curve, the output of CurvedAnimation can have a larger range than the input.

Elastic Curves such as Curves. Elasticln generate values greater than or less than the default range

Ticker

When creating an AnimationController, we pass a vsync parameter that receives an object of type TickerProvider, whose primary responsibility is to create a Ticker, as defined below:

abstract class TickerProvider {
  // Create a Ticker with a callback
  Ticker createTicker(TickerCallback onTick);
}
Copy the code

A SchedulerBinding is used to add callbacks to every screen refresh. Ticker uses a SchedulerBinding to add callbacks to screen refresh. The tIckerCallback will be called every time the screen is refreshed. Using the Ticker to drive the animation will prevent the off-screen animation (the ANIMATION’s UI is not on the current screen, such as when the screen is locked) from consuming unnecessary resources because the binding SchedulerBinding will be notified when the Flutter screen is refreshed. The Ticker is driven by SchedulerBinding and will not trigger because the screen will stop refreshing after the screen is locked.

Usually we will add SingleTickerProviderStateMixin to State definition, and then the State object as the value of the vsync it in the back of the case can see;

Tween

By default, the AnimationController object is in the range [0.0,1.0]. If we need to build a UI with animated values in a different range, or with different data types, we can use Tween to add mappings to generate values for different ranges or data types. For example, generate a value of [-200.0, 0.0]

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

The Tween constructor takes the numbers begin and end. Tween’s sole responsibility is to define the mapping from the input range to the output range. The usual input range is [0.0,1.0], which we can customize

Tween is derived from Animatable, not Animation, which mainly defines rules for mapping Animation values.

For example, mapping an animation input range to overoutput between two color values:

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

The Tween object does not store any states, ideas, and provides the Evaluate method, which retrieves the current mapping value of the animation. The current value of the Animation object can be obtained using the value method. The evaluate function also performs some additional processing, such as ensuring that animation values of 0.0 and 1.0 return start and end states, respectively.

Tween.animate

To use the Tween object, you call its animate() method and pass in a controller object that, for example, 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);
Copy the code

Note that the animate method returns an Animation, not an Animatable.

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

The above code builds a controller, a curve, and a Twwen;

The basic structure of animation

Flutter can be animated in several ways, as follows:

The most basic implementation

class AnimationTest extends StatefulWidget {
  @override
  _AnimationTestState createState() => _AnimationTestState();
}

///You need to inherit from TickerProvider, if there are multiple AnimationControllers
///TickerProviderStateMixin should be used
class _AnimationTestState extends State<AnimationTest>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 3), vsync: this);
    animation = Tween(begin: 0.0, end: 300.0).animate(controller) .. addListener(() { setState(() => {}); }); controller.forward(); }@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Animation")),
      body: Center(
        child: Image.asset("images/avatar.jpg", width: animation.value, height: animation.value),
      ),
    );
  }

  @override
  void dispose() {
    // Release animation resources when destroyed
    controller.dispose();
    super.dispose(); }}Copy the code

The above code calls setState() in addListener, so each time the animation generates a new number, the current frame table is marked as dirty, causing the Widget’s build method to be called. In build, use animation.value to change the width and height of the image, so it will gradually zoom in. It should be noted that after the animation is completed, the disponse method should be called to release the animation to prevent memory leaks.

The effect is as follows:

We specify a Curve for the animation to achieve a Curve animation, as follows:

controller =
    AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
animation = Tween(begin: 0.0, end: 300.0).animate(animation) .. addListener(() { setState(() => {}); }); controller.forward();Copy the code

Simplify using AnimatedWidget

Updating the UI with addListener and setState in the code above is a generic step that would be tedious to add to every animation.

The AnimatedWidget class encapsulates the details of setState() and allows us to isolate the widget, refactoring the code as follows:

class AnimatedImage extends AnimatedWidget {

  AnimatedImage({Key key, Animation<double> animation})
      :super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Center(
      child: Image.asset(
          "images/avatar.jpg", width: animation.value, height: animation.value), ); }}Copy the code
class AnimationTest extends StatefulWidget {
  @override
  _AnimationTestState createState() => _AnimationTestState();
}
class _AnimationTestState extends State<AnimationTest>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 3), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    animation = Tween(begin: 0.0, end: 300.0).animate(animation);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Animation")),
      body: AnimatedImage(),
    );
  }

  @override
  void dispose() {
    // Release animation resources when destroyed
    controller.dispose();
    super.dispose(); }}Copy the code

Refactor using AnimatedBuilder

Using the AnimatedWidget, we can separate the Widget from the animation, while the animation rendering process is still in the AnimatedWidget. If we add an animation that changes the transparency of the Widget, we need to implement another AnimatedWidget. This is not very elegant, and it would be nice if you could abstract out the rendering process as well;

AnimatedBuilder is precisely the method of separating 歘 from its rendering logic. The above build code can be changed to:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text("Animation")),
    body: AnimatedBuilder(
      animation: animation,
      child: Image.asset("images/avatar.jpg"),
      builder: (BuildContext context, Widget child) {
        returnCenter( child: Container( width: animation.value, height: animation.value, child: child), ); },),); }Copy the code

One confusing problem with the code above is that the child appears to be specified twice, but what actually happens is: After passing the external reference Child to the AnimatedBuilder, the AnimatedBuilder passes it to the anonymous constructor, which then uses the object as its child. The result is that the object returned by the AnimatedBuilder is inserted into the Widget tree. Directly look at the source code can be understood;

There are three benefits to writing this way:

  1. The benefits of adding frame listeners and calling setState are the same as those of AnimatedWidget;

  2. If there is no Builder, setState will be called in the context of the parent component, which will cause the build method of the parent component to be called again. With the Builder, the build of the widget itself will only be called again, avoiding unnecessary rebuild.

  3. AnimatedBuild encapsulates the transitions of the scene to recreate the animation as follows:

    class GrowTransition extends StatelessWidget {
      final Widget child;
      final Animation<double> animation;
    
      GrowTransition(this.child, this.animation);
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: AnimatedBuilder(
            animation: animation,
            child: child,
            builder: (context, child) {
              returnContainer( height: animation.value, width: animation.value, child: child, ); },),); }}Copy the code

    This encapsulates an animation that can be zoomed in on the child Widget with the following call:

    @override
    Widget build(BuildContext context) {
      return Scaffold(
          appBar: AppBar(title: Text("Animation")),
          body: GrowTransition(Image.asset("images/avatar.jpg"), animation));
    }
    Copy the code

    Flutter encapsulates many animations in this way, such as FadeTransition, ScaleTransition, SizeTransition, etc. Many times Flutter is a preset transition class that can be reused

Animation state monitor

We can add an Animattion state listener using the addStatusListener method. Flutter has four animation states defined in the ANimationStatus enumeration as follows:

Enumerated values meaning
dismissed The animation executes at the starting point
forward The animation is executing forward
reverse The animation is executing in reverse
completed The animation stops at the end

For example, to change the above enlarged animation to looping animation, only need to listen for the change of animation state, that is, the animation is reversed when the forward end, and the animation is executed when the reverse end, as follows:

void initState() {
  super.initState();
  controller =
      AnimationController(duration: const Duration(seconds: 3), vsync: this);
  animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
  animation.addStatusListener((status) {
    // Perform the reverse execution at the end
    if (status == AnimationStatus.completed) {
      controller.reverse();
    } else if (status == AnimationStatus.dismissed) {
      // In the initial state, forward execution is performedcontroller.forward(); }}); animation = Tween(begin:0.0, end: 300.0).animate(animation);
  controller.forward();
}
Copy the code

Custom route switching animation

There is a MaterialPageRoute component in the Material component library, which can be animated with a platform-style route switch, such as swiping left and right on IOS and up and down on Android. What if you want to use the left/right toggle style in Android?

The easiest way to do this is to use CupertinoPageRoute:

 Navigator.push(context, CupertinoPageRoute(  
   builder: (context)=>PageB(),
 ));
Copy the code

Cupertino Opageroute is an ios-style route switching component provided by the Cupertino component library. It slides to and from the left. How do you customize the route switching animation?

The answer is to use PageRouteBuilder. For example, if we wanted to animate the route transition with a fade in animation, the code would look like this:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text("Animation")),
    body: GestureDetector(
      child: GrowTransition(Image.asset("images/avatar.jpg"), animation),
      onTap: () {
        Navigator.push(context, PageRouteBuilder(
            pageBuilder: (context, animation, secondaryAnimation) {
          return FadeTransition(
              // Use fade inopacity: animation, child: RouteTestPage()); })); },),); }Copy the code

We can see that pageBuilder has an animation parameter provided by the Flutter route manager. Each frame is called back when the route is switched, so we can define the animation for the transition

Whether it’s MaterialPageRoute, CupertinoPageRoute, or PageRouteBuilder, they all inherit the PageRoute class, and PageRouteBuilder is just a wrapper for PageRoute, We can implement custom routing directly from the PageRoute class, as follows:

class FadeRoute extends PageRoute {
  final WidgetBuilder builder;

  ///Animation time
  @override
  final Duration transitionDuration;

  ///transparent
  @override
  final bool opaque;

  ///Can you eliminate this route by clicking on the pattern barrier?
  @override
  final bool barrierDismissible;

  @override
  final Color barrierColor;

  @override
  final String barrierLabel;

  ///Whether a route should remain in memory when it is inactive
  @override
  final bool maintainState;

  FadeRoute(
      {@required this.builder,
      this.transitionDuration = const Duration(milliseconds: 300),
      this.opaque = true.this.barrierDismissible = false.this.barrierColor,
      this.barrierLabel,
      this.maintainState = true});

  ///The main content of building this route
  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation) =>
      builder(context);

  ///Routing animation
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    // Open up new roads
    if (isActive) {
      return FadeTransition(opacity: animation, child: builder(context));
    }
    // Return, do not apply excessive animation
    returnPadding(padding: EdgeInsets.zero); }}Copy the code

Use:

Navigator.push(context, FadeRoute(builder: (context) {
  return RouteTestPage();
}));
Copy the code

Although the above two methods can achieve custom animation switch, the actual use of PageRouteBuilder should be preferred, so that there is no need to define a new routing class, it is easier to use.

Sometimes PageRouteBuilder is not able to meet the requirements, such as in the excessive animation need to obtain the attributes of the current route, this is directly through the inheritance of PageRoute, such as open route and return is not the same animation, This must determine whether the current route isActivie property is true, as shown in the merchant example;

For additional parameters, see the source code or documentation

Hero animation

Hero refers to a widget that can fly between pages. In simple terms, there is a shared widget that can switch between old and new routes during route switching. The shared widget may look different because of its location on the old page and the new page, so a Hero animation is generated as the route switches over to the position of the middle finger of the new route.

You may have seen hero animations, for example, where a route shows a thumbnail of an item for sale, and clicking on it takes you to the details. The details in the new route contain an image of the item and a buy button.

Why is this flyable shared component called hero? There is a saying that superman can fly in American culture, which is the great hero in American people’s mind. Besides, marvel superheroes can fly almost all the time. This is not the official explanation, but it is interesting

The image flying from one route to another in Flutter is called a Hero animation, although the same action is sometimes called a shared element transformation, for example:

class HeroAnimationTestA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: 50),
      alignment: Alignment.topCenter,
      child: InkWell(
        child: Hero(
          // The Hero tag must be the same for both routing pages
          tag: "avatar",
          child: ClipOval(
            child: Image.asset("images/avatar.jpg", width: 50),
          ),
        ),
        onTap: () {
          Navigator.push(context, PageRouteBuilder(
              pageBuilder: (context, animation, secondaryAnimation) {
            return FadeTransition(
                opacity: animation,
                child: Scaffold(
                  appBar: AppBar(title: Text("Original")), body: HeroAnimationTestB(), )); })); },),); }}Copy the code
class HeroAnimationTestB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Hero(
        tag: "avatar",
        child: Image.asset("images/avatar.jpg"),),); }}Copy the code

As you can see from the above code, just wrap the Widget that the Hero component will share and provide the same tag. The intermediate transition frames are automatically done by the Flutter Framework.

Note that the Hero tag must be the same. This is how the old and new routing page widgets are mapped inside the Flutter Framework.

The principle of Hero animation is simple. The Flutter Framework knows what elements and sizes are shared between the old and new routing pages, and based on these two endpoints, calculates the excessive interpolation during animation execution. Fortunately, Flutter has done this for us;

The above example looks like this: Because it is a GIF, some frames drop

Mixed animation

Sometimes we may use complex animations that consist of a sequence of animations or overlapping animations, such as an image that rotates first, then moves, or simultaneously moves and rotates. This scene contains a variety of animations. To achieve this effect, we can use Stagger Animation, which is very simple.

When using interleaved animation, note the following:

  1. Creating interlacing animations requires multiple Animation objects.
  2. An AnimationController controls all animation objects
  3. Specify the event Interval for each animation.

All animations are driven by the same AnimationController, regardless of how long the animation needs to last, the controller must be between 0.0 and 0.1, and each animation Interval must be between 0.0 and 0.1. For each property that sets the animation in the interval, create a Tween to specify the start and end values for that property. In other words, 0.0 to 1.0 represent the entire animation process, and we can specify different starting and ending points for different animations to determine when they start and end.

The sample

Implement a bar graph growth animation

  1. Isolate the animated widgets

    class StaggerAnimation extends StatelessWidget {
      final Animation controller;
      Animation<double> height;
      Animation<EdgeInsets> padding;
      Animation<Color> color;
    
      StaggerAnimation({Key key, this.controller}) : super(key: key) {
        // Height, Interval is used to specify the start and end of the animation process, the first 60% of the animation time
        height = Tween<double>(begin: . 0, end: 300.0).animate(CurvedAnimation(
            parent: controller, curve: Interval(0.0.0.6, curve: Curves.ease)));
        / / color
        color = ColorTween(begin: Colors.green, end: Colors.red).animate(
            CurvedAnimation(
                parent: controller, curve: Interval(0.0.0.6, curve: Curves.ease)));
        / / padding
        padding = Tween<EdgeInsets>(
                begin: EdgeInsets.only(left: . 0), end: EdgeInsets.only(left: 100))
            .animate(CurvedAnimation(
                parent: controller, curve: Interval(0.6.1.0, curve: Curves.ease)));
      }
    
      Widget _buildAnimation(BuildContext context, Widget child) {
        return Container(
            alignment: Alignment.bottomCenter,
            padding: padding.value,
            child: Container(
              color: color.value,
              width: 50.0,
              height: height.value,
            ));
      }
    
      @override
      Widget build(BuildContext context) {
        returnAnimatedBuilder(animation: controller, builder: _buildAnimation); }}Copy the code

    Three animations are defined, namely height, color, and inner margin, and Interval is used to specify the start and end of the entire animation

  2. Using that animation

    class StaggerTest extends StatefulWidget {
      @override
      _StaggerTestState createState() => _StaggerTestState();
    }
    
    class _StaggerTestState extends State<StaggerTest>
        with TickerProviderStateMixin {
      AnimationController _controller;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(
            duration: const Duration(milliseconds: 2000), vsync: this);
      }
    
      _playAnimation() async {
        try {
          // Perform forward animation
          await _controller.forward().orCancel;
          // Perform the animation backwards
          await _controller.reverse().orCancel;
        } on TickerCanceled {
          // The animation was cancelled, probably because we were processed}}@override
      Widget build(BuildContext context) {
        return GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTap: () {
            _playAnimation();
          },
          child: Center(
            child: Container(
              width: 300.0,
              height: 300.0,
              decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.1),
                  border: Border.all(color: Colors.black.withOpacity(0.5))), child: StaggerAnimation(controller: _controller), ), ), ); }}Copy the code

    The effect is shown below

    In fact, interlacing animation is to put multiple animations together and use a controller to control;

General purpose animation component

In the actual development process, we often encounter scenarios of switching UI elements, such as Tab switching, route switching, etc. To enhance the user experience, it is common to specify an animation when switching to make the switching process smooth. The Flutter SDK provides the following commonly used switch components, such as PageView, TabView, etc. However, these components do not cover all required scenarios. For this purpose, the Flutter SDK provides an AnimatedSwitch component. It defines a general UI switch abstraction.


AnimatedSwitch

AnimatedSwitch can add display and hide animation to both new and old child elements. That is, when a child element of the AnimatedSwitch changes, the old element and the new element are treated. The definition is as follows:

const AnimatedSwitcher({
  Key key,
  this.child,
  @required this.duration, // The new child displays the animation length
  this.reverseDuration,// The animation length of the old child hidden
  this.switchInCurve = Curves.linear, // The new child displays the animation curve
  this.switchOutCurve = Curves.linear,// Old child hides the animation curve
  this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // Animation builder
  this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, // Layout builder
})
Copy the code

When the child of the AnimatedSwitch changes (of a different type or key), the old child performs a hidden animation and the new child performs a display animation. Animation effects are decided by transitionBuilder parameters, namely the parameter receives a AnimatedSwitchTransitionBUilder type builder, are defined as follows:

typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
Copy the code

The Builder will animate new and old children when toggling the child of AnimatedSwitch

1, the animation bound to the old child will be executed in reverse

2. The animation bound to the new child will be performed forward.

In this way, the new and old child animations are bound. AnimatedSwitch default value is AnimatedSwitch. DefaultTransitionBuilder:

Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
  return FadeTransition(
    opacity: animation,
    child: child,
  );
}
Copy the code

You can see that the FadeTransition object is returned, that is, the AnimatedSwitch performs fade and fade animations on the old child by default.

Chestnut:

Implement a counter, in the process of each increment, the old number shrink hide, the new number magnify display, as follows:

class AnimatedSwitcherTest extends StatefulWidget {
  AnimatedSwitcherTest({Key key}) : super(key: key);

  @override
  _AnimatedSwitcherTestState createState() =>
      _AnimatedSwitcherTestState();
}

class _AnimatedSwitcherTestState
    extends State<AnimatedSwitcherTest> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          AnimatedSwitcher(
            duration: const Duration(milliseconds: 500),
            transitionBuilder: (Widget child, Animation<double> animation) {
              // Perform the zoom animation
              return ScaleTransition(scale: animation, child: child,);
            },
            // Specify the key explicitly. Different keys are treated as different Text so that the animation can be performed
            child: Text("$_count",
                key: ValueKey<int>(_count),
                style: Theme.of(context).textTheme.headline4),
          ),
          RaisedButton(
              child: const Text('+ 1'),
              onPressed: () {
                setState(() {
                  _count += 1; }); })],),); }}Copy the code

The effect is as follows:

Note: The new and old children of the AnimatedSwitcher may have to be different if they are of the same type, because only different keys are considered different texts in order to perform animation

AnimatedSwitch implementation principle

To switch between old and new child animations, just one question needs to be addressed: how will the animation be executed on the old and new child

As you can see from the AnimatedSwitch, when the Child changes (if the key and type of the child widget are not equal), the build is re-executed and the animation begins. We can implement the AnimatedSwitch by inheriting the StatefulWidget by determining whether the old and new child have changed in the didUpdateWidget, and if so, Reverse animation is performed on the old child and forward animation is performed on the new child. Here are some of the core pseudocode for the AnimatedSwitch implementation:

Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
  super.didUpdateWidget(oldWidget);
  // Check whether the new and old child have changed (return true if the key and type are equal)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    // Child does not change...
  } else {
    // The child is changed, and a Stack is constructed to animate the old and new child separately
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        // Old child application FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        // New child application FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // Perform a reverse exit animation for the old child
    _controllerOldAnimation.reverse();
    // Animate the forward entry for the new child_controllerNewAnimation.forward(); }}/ / the build method
Widget build(BuildContext context){
  return _widget;
}
Copy the code

The pseudo code above shows the core logic of AnimatedSwitcher, of course the real logic is more complex than this, it can customize the exit transition animation has executed the layout of the animation, etc. Here, we through the pseudo code is mainly to see the main implementation ideas;

In addition, the Flutter SDK provides an AnimatedCrossFade component that can also switch between two child elements and perform fade and fade animations while switching. Unlike the AnimagedSwticher, the AnimatedCrossFade works with two child elements, whereas the AnimatedSwitch switches between the old and new values of a child element.

AnimatedSwitch Advanced usage

If we want to implement an animation similar to a route pan: the old page screen exits to the left, and the new page enters from the right side of the screen. Failing that, as we’ll soon find out, we might write code like this:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween=Tween<Offset>(begin: Offset(1.0), end: Offset(0.0))
     returnSlideTransition( child: child, position: tween.animate(animation), ); },.../ / to omit
)
Copy the code

The problem with the above code is that AnimatedSwitch’s Child will animate forward for the new child and backward for the old child, so the real effect is that the new child is panned in from the right side of the screen. But the old child exits not from the left, but from the right. Because without special processing, the forward and reverse sides of the same animation are exactly opposite (symmetric).

So the question is, can’t we use AnimatedSwitch? , the answer is no. The reason is that Animation is symmetric, so we just need to break this rule. Next, we will encapsulate a MySlideTransition. The only difference with SlideTransition is that it customizes the reverse execution of the animation (hidden from the left) as follows:

class MySlideTransition extends AnimatedWidget {
  final bool transformHitTests;
  final Widget child;

  Animation<Offset> get position => listenable;

  MySlideTransition(
      {Key key,
      @required Animation<Offset> position,
      this.transformHitTests = true.this.child})
      : assert(position ! =null),
        super(key: key, listenable: position);

  @override
  Widget build(BuildContext context) {
    Offset offset = position.value;
    if (position.status == AnimationStatus.reverse) {
      offset = Offset(-offset.dx, offset.dy);
    }
    returnFractionalTranslation( translation: offset, transformHitTests: transformHitTests, child: child); }}Copy the code
AnimatedSwitcher(
  duration: const Duration(milliseconds: 500),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween = Tween(begin: Offset(1.0), end: Offset(0.0));
    // Perform the zoom animation
    return MySlideTransition(
      position: tween.animate(animation),
      child: child,
    );
  },
  // Specify the key explicitly. Different keys are treated as different Text so that the animation can be performed
  child: Text("$_count",
      key: ValueKey<int>(_count),
      style: Theme.of(context).textTheme.headline4),
)
Copy the code

The effect is shown in the figure above. Flutter routing is also implemented by AnimatedSwtcher

SlideTransitionX

We implement this in and out sliding animation by encapsulating a generic SlideTransitionX as follows:

class SlideTransitionX extends AnimatedWidget {
  Animation<double> get position => listenable;
  final bool transformHitTests;
  final Widget child;

  // Exit/exit direction
  final AxisDirection direction;
  Tween<Offset> _tween;

  SlideTransitionX(
      {Key key,
      @required Animation<double> position,
      this.transformHitTests = true.this.direction = AxisDirection.down,
      this.child})
      : assert(position ! =null),
        super(key: key, listenable: position) {
    switch (direction) {
      case AxisDirection.up:
        _tween = Tween(begin: Offset(0.1), end: Offset(0.0));
        break;
      case AxisDirection.right:
        _tween = Tween(begin: Offset(- 1.0), end: Offset(0.0));
        break;
      case AxisDirection.down:
        _tween = Tween(begin: Offset(0.- 1), end: Offset(0.0));
        break;
      case AxisDirection.left:
        _tween = Tween(begin: Offset(1.0), end: Offset(0.0));
        break; }}@override
  Widget build(BuildContext context) {
    Offset offset = _tween.evaluate(position);
    if (position.status == AnimationStatus.reverse) {
      switch (direction) {
        case AxisDirection.up:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.right:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case AxisDirection.down:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.left:
          offset = Offset(-offset.dx, offset.dy);
          break; }}returnFractionalTranslation( translation: offset, transformHitTests: transformHitTests, child: child); }}Copy the code
AnimatedSwitcher(
  duration: const Duration(milliseconds: 500),
  transitionBuilder: (Widget child, Animation<double> animation) {
    return SlideTransitionX(
      direction: AxisDirection.down,
      position: animation,
      child: child,
    );
  },
  // Specify the key explicitly. Different keys are treated as different Text so that the animation can be performed
  child: Text("$_count",
      key: ValueKey<int>(_count),
      style: Theme.of(context).textTheme.headline4),
),
Copy the code

The effect is as follows:

Animation transition component

For the sake of presentation, we refer to the component that overanimates when the widget property changes as an “overanimated component.” One of the most obvious features of overanimating is that it manages its own AnimationController internally. We specify that the user can customize the duration, curve, etc of the animation, which is usually provided by the user. However, the user would have to manually manage the AnimationController, which would add complexity. Therefore, if the AnimationController can be wrapped, it will greatly improve the ease of use of animation components.

Custom animation transition components

We implement an AnimatedDecoratedBox that performs a transition animation as the Decorated property changes from the old state to the new state. Based on the implementation learned above, we write the following code:

class AnimatedDecoratedBox1 extends StatefulWidget {
  final BoxDecoration decoration;
  final Widget child;

  // Execution time
  final Duration duration;

  / / curve
  final Curve curve;

  // Reverse execution time
  final Duration reverseDuration;

  AnimatedDecoratedBox1(
      {Key key,
      @required this.decoration,
      this.child,
      @required this.duration,
      this.reverseDuration,
      this.curve = Curves.linear});

  @override
  _AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
}

class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;
  DecorationTween _tween;

  @protected
  AnimationController get controller => _controller;

  @protected
  Animation get animation => _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: widget.duration,
        reverseDuration: widget.reverseDuration,
        vsync: this);
    _tween = DecorationTween(begin: widget.decoration);
    _updateCurve();
  }

  void _updateCurve() {
    if(widget.curve ! =null) {
      _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
    } else{ _animation = _controller; }}@override
  void didUpdateWidget(covariant AnimatedDecoratedBox1 oldWidget) {
    super.didUpdateWidget(oldWidget);
    if(widget.curve ! = oldWidget.curve) _updateCurve(); _controller.duration = widget.duration; _controller.reverseDuration = widget.reverseDuration;if(widget.decoration ! = (_tween.end ?? _tween.begin)) { _tween .. begin = _tween.evaluate(_animation) .. end = widget.decoration; _controller .. value =0.0. forward(); }}@override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return DecoratedBox(
            decoration: _tween.animate(_animation).value, child: child);
      },
      child: widget.child,
    );
  }

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

The effect is as follows:

Although the above code achieves the desired function, it is a bit complicated. In fact, the AnimationController management and Tween code can be extracted. If this part of code is encapsulated as a base class, then the transition component can be implemented only by inheriting the base class. Then you can customize your own different DIAM. This will greatly simplify the code;

In the Flutter SDK provides a ImplicitlyAnimatedWidgetState class, he inherited from StatefulState, also provides a corresponding ImplicitlyAnimatedWidgetState class, The AnimationController is managed in this class. Developers to encapsulate the movie, you just need to inherit ImplicitlyAnimatedWidget and ImplicitlyAnimatedWidgetState class;

We need to do this in two steps:

  1. Inherited ImplicitlyAnimatedWidget class

    
    class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
      final BoxDecoration decoration;
      final Widget child;
    
      AnimatedDecoratedBox(
          {Key key,
          @required this.decoration,
          this.child,
          Curve curve = Curves.linear, // Animate the curve
          @required Duration duration, // Animation execution time
          Duration reverseDuration})
          : super(key: key, curve: curve, duration: duration);
    
      @override
      ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() =>
          _AnimatedDecoratedBoxState();
    }
    Copy the code

    Curve, Duration attributes are defined in the parent class, and you can see that they are no different from normal classes that inherit from StatefulWidget.

  2. The State class inherits from AnimatedWidgetBaseState (the class inherits from ImplicitlyAnimatedWidgetState class)

    class _AnimatedDecoratedBoxState
        extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
      DecorationTween _decorationTween;
    
      @override
      Widget build(BuildContext context) {
        return DecoratedBox(
          decoration: _decorationTween.evaluate(animation),
          child: widget.child,
        );
      }
    
      @override
      void forEachTween(visitor) {
        // The base class calls this method when it needs to update the Tween_decorationTween = visitor(_decorationTween, widget.decoration, (value) => DecorationTween(begin: value)); }}Copy the code

    We implement the build and forEachTween methods.

    During the execution of the animation, the build method is called for each frame (the call logic is in the parent class), so we need to build the DecoratedBox state of each frame in the build method, so we need to calculate the decoration state of each frame. This we can by _decoraitionTween. Evaluate (animation) to calculate, the animation is ImplicitlyAnimatedSidgetState base class defined in the object, DecoratioNTween is a custom object of type DecoratioNTween. When is _decorationTween assigned? Decorateiontween is a Tween that defines the start and end states of the animation. For AnimatedDecoratedBox, the ending state of decoration is the value passed to it by the user. And the initial state is indeterminate, there are two cases:

    1. The AnimatedDecoratedBox is built for the first time, and its DecorationTween value is set to DecorationTween(begin:decoration).
    2. When AnimatedDecoratedBox decoration is updated, the initial state is _decorated.animate (animation), The _decorationTween value is DecorationTween(begin:_decoration.animate(animation),end:decoration).

    Now the forEachTween function is obvious, because it’s what we’re doing to update the initial Tween value. In both cases, we just need to override the method and update the Tween’s starting state value in this method. While some of the updated logic is masked in the visitor callback, we just need to pass it the correct parameters.

    Tween visitor(
         Tween<dynamic> tween, // The current tween is called null for the first time
         dynamic targetValue, // Terminate state
         TweenConstructor<dynamic> constructor,// The Tween constructor, which is called in all three cases to update the Tween
       );
    Copy the code

Flutter presets animation transition components

The Flutter SDK presets many transition components in much the same way as AnimatedDecoratedBox.

Component name function
AnimatedPadding A transition animation is performed to the new state when the padding changes
AnimatedPositioned Used in conjunction with Stack, transition animation is performed to the new state when the positioning state changes
AnimatedOpactity Perform a transition animation to the new state when opacity changes
AnimatedAlign A transition animation is performed to the new state when aligment changes
AnimatedContainer The transition animation is performed to the new state when the Container property changes
AnimatedDefaultTextStyle When the font style changes, the text component inheriting the style in the child component transitions dynamically to the new style

Example:

class AnimatedWidgetsTest extends StatefulWidget {
  @override
  _AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
}

class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
  double _padding = 10;
  Alignment _align = Alignment.topRight;
  double _height = 100;
  double _left = 0;
  Color _color = Colors.red;
  TextStyle _style = TextStyle(color: Colors.black);
  Color _decorationColor = Colors.blue;

  @override
  Widget build(BuildContext context) {
    var duration = Duration(seconds: 5);
    return SingleChildScrollView(
      child: Column(
        children: [
          RaisedButton(
            onPressed: () => setState(() => _padding = 20),
            child: AnimatedPadding(
              duration: duration,
              padding: EdgeInsets.all(_padding),
              child: Text("AnimatedPadding"),
            ),
          ),
          SizedBox(
            height: 50,
            child: Stack(
              children: [
                AnimatedPositioned(
                  child: RaisedButton(
                    onPressed: () => setState(
                      () => _left = 100,
                    ),
                    child: Text("AnimatedPositioned"),
                  ),
                  duration: duration,
                  left: _left,
                ),
              ],
            ),
          ),
          Container(
            height: 100,
            color: Colors.grey,
            child: AnimatedAlign(
              duration: duration,
              alignment: _align,
              child: RaisedButton(
                onPressed: () => setState(() => _align = Alignment.center),
                child: Text("AnimatedAlign"),
              ),
            ),
          ),
          AnimatedContainer(
            duration: duration,
            height: _height,
            color: _color,
            child: FlatButton(
              onPressed: (() => setState(() => this
                .._height = 150
                .._color = Colors.blue)),
              child: Text("AnimatedContainer",
                  style: TextStyle(color: Colors.white)),
            ),
          ),
          AnimatedDefaultTextStyle(
              child: GestureDetector(
                  child: Text("hello world"),
                  onTap: () => _style = TextStyle(
                      color: Colors.blue,
                      decorationStyle: TextDecorationStyle.solid,
                      decorationColor: Colors.blue)),
              style: _style,
              duration: duration),
          AnimatedDecoratedBox(
            decoration: BoxDecoration(color: _decorationColor),
            duration: duration,
            child: FlatButton(
              onPressed: () => setState(() => _decorationColor = _color),
              child: Text("AnimatedDecoratedBox",
                  style: TextStyle(color: Colors.white)),
            ),
          )
        ]
            .map((e) => Padding(
                  padding: EdgeInsets.symmetric(vertical: 16), child: e, )) .toList(), ), ); }}Copy the code

The effect is as follows: