Flutter Notes | Flutter Animations

Animation abstraction in Flutter

In order to make it easier for developers to create animations, different UI systems abstract animations. Flutter also abstracts animations, mainly involving Animation、Curve、Controller、Tweenthese four characters. They work together to complete a complete animation. Let’s introduce them one by one. they.

1. Animation

AnimationIs an abstract class, which has nothing to do with UI rendering itself, and its main function is to save the interpolation and state of animation; one of the more commonly used Animationclasses is Animation<double>.

AnimationTweenAn object is a class that sequentially generates values ​​between an interval ( ) over a period of time . AnimationThe value output by the object during the entire animation execution can be linear, curved, a step function or any other curved function, etc., Curvedepending on decision.

Depending on Animationhow the object is controlled, the animation can run forward (starting at the start state and ending at the end state), or in reverse, or even switch directions in between.

AnimationIt is also possible to generate doubleother types of values ​​besides, such as: Animation<Color>or Animation<Size>. In each frame of the animation, we can get the current state value of the animation through the properties Animationof the object .value

animation notification

We can Animationmonitor each frame of the animation and the change of the execution state Animationthrough the following two methods:

  • addListener(); it can be used to Animationadd a frame listener, which will be called every frame. The most common behavior in a frame listener is called after a state change setState()to trigger a UI rebuild.
  • addStatusListener(); it can Animationadd an "animation state change" listener; the AnimationStatusstate change listener will be called when the animation starts, ends, forward or reverse (see definition).

Here you only need to know the difference between the frame monitor and the status monitor, and we will illustrate it with an example later.

2. Curve

The animation process can be at a constant speed, uniformly accelerated, or first accelerated and then decelerated. Flutter uses Curve(curve) to describe the animation process. We call uniform animation linear ( Curves.linear), and non-uniform animation non-linear.

We can CurvedAnimationspecify the curve of the animation by, such as:

final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationand AnimationController(described below) are Animation<double>both types. CurvedAnimationWe can wrap AnimationControllerand Curvegenerate a new animation object, which is how we associate the animation with the curve that the animation executes. The curve we specify for the animation is Curves.easeIn, which means that the animation starts slower and ends faster. CurvesThe class is a preset enumeration class, which defines many commonly used curves. The following are some commonly used ones:

Curves animation process
linear Uniform
decelerate Uniform deceleration
ease Start to accelerate, then slow down
easeIn start slow, then fast
easeOut start fast, then slow
easeInOut Start slow, then speed up, then slow down again

In addition to the above listed, Curvesthere are many other curves defined in the class, you can check Curvesthe class definition yourself.

Of course we can also create our own Curve, for example we define a sine curve:

class ShakeCurve extends Curve {
    
    
  
  double transform(double t) {
    
    
    return math.sin(t * math.PI * 2);
  }
}

3. AnimationController

AnimationControllerIt is used to control the animation, which includes methods for starting forward(), stopping stop(), and playing in reverse . Every frame of the animation, a new value is generated. By default, the numbers from to (the default interval) are generated linearly over the given time period .reverse()AnimationControllerAnimationController0.01.0

For example, the following code creates an Animationobject (but does not start an animation):

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

Among them, durationit represents the duration of the animation execution, through which we can control the speed of the animation.

AnimationControllerThe range of generated numbers can be specified by lowerBoundand upperBound, such as:

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

AnimationControllerDerived from Animation<double>, so it can Animationbe used anywhere an object is expected.

What's more, AnimationControllerthere are other methods to control the animation, such as forward()methods can start forward animation, and reverse()can start reverse animation. After the animation starts to execute, the animation frame is generated. Every time the screen is refreshed, it is an animation frame. In each frame of the animation, the current animation value ( ) will be generated according to the animation curve, and then constructed according to the current animation value Animation.value. UI, when all animation frames are triggered sequentially, the animation value will change sequentially, so the built UI will also change sequentially, so finally we can see a completed animation. In addition, at each frame of the animation, Animationthe object will call its frame listener, and when the animation state changes (such as the end of the animation), it will call the state change listener.

Note: In some cases, animation values ​​may be out of AnimationControllerrange [0.0,1.0], depending on the specific curve. For example, fling()the function can simulate a finger throwing animation according to the speed ( velocity) and force ( force) of our finger sliding (throwing out), so its animation value can be [0.0,1.0]out of range. That is, depending on the curve chosen, CurvedAnimationthe output of the can have a larger range than the input. For example, Curves.elasticInisoelastic curves generate values ​​that are larger or smaller than the default range.

Ticker

When creating one AnimationController, you need to pass a vsyncparameter, which receives a TickerProvidertype of object whose main responsibility is to create Ticker, defined as follows:

abstract class TickerProvider {
    
     
  Ticker createTicker(TickerCallback onTick); // 通过一个回调创建一个Ticker
}

The Flutter application will bind one when it is started SchedulerBinding, through which you SchedulerBindingcan add a callback to each screen refresh, and add a screen refresh callback through , so that it will be called every time the screen refreshes .TickerSchedulerBindingTickerCallback

Using Ticker(instead of Timer) to drive the animation will prevent off-screen animation (when the animated UI is not on the current screen, such as when the screen is locked) from consuming unnecessary resources, because when the screen refreshes in Flutter, it will be notified to the bound, butSchedulerBinding driven Yes, since the screen will stop refreshing after the screen is locked, it will not be triggered again.TickerSchedulerBindingTicker

Usually we will SingleTickerProviderStateMixinmix the class into the custom Stateclass, and then use the current Stateobject as the value AnimationControllerof vsyncthe parameter.

4. Tween

1 Introduction

By default, AnimationControllerthe range of object values ​​is [0.0,1.0]. If we need to build UI animation values ​​in different ranges or different data types, we can use Tweento add mappings to generate values ​​in different ranges or data types.

For example, as in the following example, the Tweengenerated [-200.0,0.0]value:

final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);

TweenThe constructor takes beginand endtwo parameters. TweenThe sole responsibility of is to define the mapping from input ranges to output ranges. The input range is usually [0.0,1.0], but this is not required, we can customize the required range.

TweenInherit from Animatable<T>, rather than inherit from Animation<T>, Animatablemainly defines the mapping rules for animation values.

Let's look at an ColorTweenexample of mapping an animation input range to a transition output between two color values:

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

TweenThe object does not store any state, instead, it provides evaluate(Animation<double> animation)methods, which can get the animation's current mapping value. AnimationThe current value of the object can value()be obtained through the method. The function also does some additional processing, such as ensuring that the start and end states are returned when the animation value is and evaluate, respectively .0.01.0

2)Tween.animate

To use Tweenan object, you call its animate()method, passing in an AnimationControllerobject.

For example, the following code generates integer values ​​from to in 500milliseconds .0255

final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 500), 
  vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Note animate()that one is returned Animation, not one Animatable.

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

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

Linear interpolation lerp function

The principle of animation is actually to draw different content in each frame. Generally, the start and end states are specified, and then gradually change from the start state to the end state within a period of time, and the state value of a specific frame will be based on the animation. Therefore, Flutter defines static lerpmethods (linear interpolation) for some state properties that may be animated , such as:

// a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);

lerpThe calculation of generally follows: 返回值 = a + (b - a) * t, and other lerpclasses with methods:

Size.lerp(a, b, t)
Rect.lerp(a, b, t)
Offset.lerp(a, b, t)
Decoration.lerp(a, b, t)
Tween.lerp(t) // 起始状态和终止状态在构建 Tween 的时候已经指定了
...

It should be noted that lerpit is a linear interpolation, which means that the return value and the animation progress tare in a linear function ( y = kx + b) relationship, because the image of a linear function is a straight line, so it is called linear interpolation.

If we want the animation to execute according to a curve, we can tmap to , for example, to achieve a uniform acceleration effect, then t' = at²+bt+cspecify the acceleration aand b(in most cases, it is necessary to ensure t'that the value range of is [0,1], of course, there are some cases that may be will exceed this value range, such as the spring ( bounce) effect), and Curvethe principle of performing animation according to different curves is essentially trealized by mapping according to different mapping formulas.

animation basic structure

In Flutter, we can implement animation in a variety of ways. The following uses a different implementation of an example of gradually zooming in on a picture to demonstrate the difference between different implementations of animation in Flutter.

1. Basic version

Let's demonstrate the most basic animation implementation method:

class ScaleAnimationRoute extends StatefulWidget {
    
    
  const ScaleAnimationRoute({
    
    Key? key}) : super(key: key);
  
  _ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}

// 需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin {
    
    
  late Animation<double> animation;
  late AnimationController controller;
  
  
  initState() {
    
    
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    // 匀速 图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
    
    
        setState(() => {
    
    });
      });
    // 启动动画(正向执行)
    controller.forward();
  }

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Image.asset(
       "imgs/avatar.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }
  
  
  dispose() {
    
     
    controller.dispose(); // 路由销毁时需要释放动画资源
    super.dispose();
  }
}

The function in the above code addListener()is called setState(), so every time the animation generates a new number, the current frame is marked as dirty ( dirty), which will cause widgetthe build()method to be called again, and in build()it, Imagethe width and height of the change, because its height and The width is now used animation.value, so it will be gradually enlarged.

It is worth noting that the controller is released (calling dispose()the method) when the animation is complete to prevent memory leaks.

In the above example, it is not specified Curve, so the zoom-in process is linear (uniform speed). Next, we specify one Curveto achieve an animation process similar to a spring effect. We only need to initStatechange the code in the following to the following:


initState() {
    
    
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 3), vsync: this);
    // 使用弹性曲线
    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    // 图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
    
    
        setState(() => {
    
    });
      });
    // 启动动画
    controller.forward();
  }

running result:

insert image description here

2. Use AnimatedWidget to simplify

We found that the step of updating the UI through addListener()and in the above example setState()is actually common, and it would be cumbersome to add such a sentence to each animation. AnimatedWidgetThe class encapsulates setState()the details of the call and allows us to widgetseparate out. The refactored code is as follows:

import 'package:flutter/material.dart';

class AnimatedImage extends AnimatedWidget {
    
    
  const AnimatedImage({
    
    Key? key, required Animation<double> animation,}) : super(key: key, listenable: animation);

  
  Widget build(BuildContext context) {
    
    
    final animation = listenable as Animation<double>;
    return  Center(
      child: Image.asset(
        "imgs/avatar.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }
}

class ScaleAnimationRoute extends StatefulWidget {
    
    
  const ScaleAnimationRoute1({
    
    Key? key}) : super(key: key);
  
  _ScaleAnimationRouteState createState() =>  _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin {
    
    
  late Animation<double> animation;
  late AnimationController controller;

  
  initState() {
    
    
    super.initState();
    controller =  AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // 图片宽高从0变到300
    animation =  Tween(begin: 0.0, end: 300.0).animate(controller);
    // 启动动画
    controller.forward();
  }

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

  
  dispose() {
    
     
    controller.dispose(); // 路由销毁时需要释放动画资源
    super.dispose();
  }
}

3. Refactoring with AnimatedBuilder

Use AnimatedWidgetcan be separated from the animation widget, and the rendering process of the animation (that is, setting the width and height) is still in AnimatedWidget, assuming that if we add an widgetanimation of transparency change, then we need to implement another one AnimatedWidget, which is not very elegant. If we can put The rendering process is also abstracted, it will be much better, and AnimatedBuilderit is to separate the rendering logic, buildthe code in the above method can be changed to:


Widget build(BuildContext context) {
    
    
    // return AnimatedImage(animation: animation,);
    return AnimatedBuilder(
      animation: animation,
      child: Image.asset("imgs/avatar.png"),
      builder: (BuildContext ctx, child) {
    
    
        return  Center(
          child: SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          ),
        );
      },
    );
}

One confusing problem with the above code is that childit looks like it is specified twice. But what's actually happening is that the external reference childis passed to the anonymous constructor, which then uses the object as its child AnimatedBuilder. AnimatedBuilderThe end result is that AnimatedBuilderthe returned object is inserted into widgetthe tree.

Maybe you will say that this is not much different from our initial example, but it will bring three benefits:

  1. There is no need to explicitly add a frame listener and then call it setState(), the benefit is AnimatedWidgetthe same as that of .

  2. Better performance: Because widgetthe scope of each frame of the animation needs to be constructed is narrowed, if not builder, setState()it will be called in the context of the parent component, which will cause buildthe method of the parent component to be called again; after having builderit, it will only cause animation widgetSelf- buildrecall, avoiding unnecessary rebuild.

  3. Animations can be reused by AnimatedBuilderencapsulating common transition effects. Let's GrowTransitionillustrate by encapsulating one, which can widgetrealize zoom-in animation for children:

class GrowTransition extends StatelessWidget {
    
    
  const GrowTransition({
    
    Key? key, required this.animation, this.child,}) : super(key: key);

  final Widget? child;
  final Animation<double> animation;

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (BuildContext context, child) {
    
    
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

This way, the original example can be changed to:

...
Widget build(BuildContext context) {
    
    
  return GrowTransition(
    child: Image.asset("images/avatar.png"), 
    animation: animation,
  );
}

It is in this way that Flutter encapsulates many animations, such as: FadeTransition, ScaleTransition, SizeTransitionetc. These preset transition classes can be reused in many cases.

animation state monitoring

As mentioned above, we can add animation state change listeners through the method Animation. addStatusListener()In Flutter, there are four animation states, AnimationStatuswhich are defined in the enumeration class, and we will explain them one by one below:

enumeration value meaning
dismissed Animation stops at start point
forward The animation is executing in the forward direction
reverse animation is being performed in reverse
completed animation stops at end

Example: Let's change the above image zoom-in example to a loop animation that zooms in first, then zooms out, and then zooms in again. To achieve this effect, we only need to monitor the change of the animation state, that is, reverse the animation at the end of the forward execution of the animation, and execute the animation forward at the end of the reverse execution of the animation. code show as below:

initState() {
    
    
  super.initState();
  controller = AnimationController(
    duration: const Duration(seconds: 1), 
    vsync: this,
  );
  animation = Tween(begin: 0.0, end: 300.0).animate(controller); // 图片宽高从0变到300
  animation.addStatusListener((status) {
    
    
    if (status == AnimationStatus.completed) {
    
     
      controller.reverse();  // 动画执行结束时反向执行动画
    } else if (status == AnimationStatus.dismissed) {
    
     
      controller.forward(); // 动画恢复到初始状态时执行动画(正向)
    }
  }); 
  controller.forward();  // 启动动画(正向)
}

Custom route switching animation

The Material component library provides a MaterialPageRoutecomponent that can use routing switching animations consistent with the platform style, such as sliding left and right on iOS, and sliding up and down on Android. Now, if we Androidalso want to use the left and right switching styles on the screen, what should we do? A simple approach can be used directly CupertinoPageRoute, such as:

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

CupertinoPageRouteIt is Cupertinoan iOS-style routing switching component provided by the component library, which realizes left and right sliding switching. So how do we customize the routing switching animation? The answer is PageRouteBuilder.

Let's take a look at how to use PageRouteBuilderto customize the routing switching animation. For example, if we want to realize the routing transition with fade-in animation, the implementation code is as follows:

Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500), // 动画时间为500毫秒
    pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) {
    
    
      return FadeTransition( // 使用渐隐渐入过渡
        opacity: animation,
        child: PageB(), // 路由B
      );
    },
  ),
);

We can see pageBuilderthat there is a parameter, which is provided by the Flutter route manager, and it will be called back at each animation frame animationwhen the route is switched , so we can customize the transition animation through the object.pageBuilderanimation

Whether they are MaterialPageRoute, CupertinoPageRoute, or PageRouteBuilder, they all inherit from the PageRouteclass, but they PageRouteBuilderare actually just PageRoutea wrapper. We can directly inherit PageRoutethe class to implement custom routing. The above example can be implemented as follows:

  1. define a routing classFadeRoute
class FadeRoute extends PageRoute {
    
    
  FadeRoute({
    
    
    required this.builder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  });

  final WidgetBuilder builder;

  
  final Duration transitionDuration;

  
  final bool opaque;

  
  final bool barrierDismissible;

  
  final Color barrierColor;

  
  final String barrierLabel;

  
  final bool maintainState;

  
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) => builder(context);

  
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    
    
     return FadeTransition( 
       opacity: animation,
       child: builder(context),
     );
  }
}
  1. useFadeRoute
Navigator.push(context, FadeRoute(builder: (context) {
    
    
  return PageB();
}));

Although the above two methods can realize custom switching animations, they should be used first in actual use PageRouteBuilder, so that there is no need to define a new routing class, and it will be more convenient to use.

But sometimes PageRouteBuilderit cannot meet the requirements. For example, when applying transition animation, we need to read some properties of the current route. At this time, we can only use inheritance PageRoute. For example, if we only want to apply when opening a new route Animation, but do not use animation when returning, then we must judge isActivewhether the current routing property is when we build the transition animation true, the code is as follows:


Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
    
     
	 if (isActive) {
    
      // 当前路由被激活,是打开新路由
	   return FadeTransition(
	     opacity: animation,
	     child: builder(context),
	   );
	 } else {
    
     // 是返回,则不应用过渡动画
	   return Padding(padding: EdgeInsets.zero);
	 }
}

For detailed information about routing parameters, you can refer to the API documentation by yourself, which is relatively simple and will not be repeated here.

Hero animation

HeroIt refers to the ability to "fly" between routes (pages) widget. In simple terms, Heroanimation means that when routes are switched, there is a shared one widgetthat can switch between old and new routes. Since the shared widgetposition and appearance on the old and new route pages may be different, when the route is switched, it will gradually transition from the old route to the specified position in the new route, which will generate an animation Hero.

You may have seen heroanimation many times. For example, one route displays a thumbnail list of items for sale, and selecting an entry jumps them to a new route that contains the item's details and a "Buy" button. "Flying" an image from one route to another in Flutter is called a hero animation , although the same action is sometimes called a shared element transition . Let's use an example to experience heroanimation.

example

Suppose there are two routes A and B, and their content interaction is as follows:

A: Contains a user avatar, circular, jump to route B after clicking, you can view a larger image.

B: Display the original image of the user's avatar, a rectangle.

When jumping between the two routes of AB, the user's avatar will gradually transition to the avatar of the target routing page. Next, let's look at the code first, and then analyze it.

Route A:

class HeroAnimationRouteA extends StatelessWidget {
    
    
  const HeroAnimationRouteA({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(title: const Text("Hero动画"),),
      body: Container(
        alignment: Alignment.topCenter,
        child: InkWell(
          child: Hero(
            tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
            child: ClipOval(
              child: Image.asset("images/avatar.png", width: 100.0,),
            ),
          ),
          onTap: () {
    
    
            // 打开B路由
            Navigator.push(context, PageRouteBuilder(
              transitionDuration: const Duration(milliseconds: 500),
              pageBuilder: (context, animation, secondaryAnimation) {
    
    
                return FadeTransition(
                  opacity: animation,
                  child: const HeroAnimationRouteB(),
                );
              },
            ));
          },
        ),
      ),
    );
  }
}

Route B:

class HeroAnimationRouteB extends StatelessWidget {
    
    
  const HeroAnimationRouteB({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
     //appBar: AppBar(title: const Text("原图"),),
      body: Center(
        child: Hero(
          tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
          child: Image.asset("images/avatar.png"),
        ),
      ),
    );
  }
}

Effect:

insert image description here

We can see that to realize Herothe animation, you only need Heroto wrap the shared with components widgetand provide the same tag, and the transition frames in the middle are all automatically completed by the Flutter framework. It must be noted that the shared of the front and rear routing pages must be the sameHerotag , and the Flutter framework uses tagto determine widgetthe corresponding relationship between the old and new routing pages.

HeroThe principle of animation is relatively simple. The Flutter framework knows the position and size of the shared elements in the old and new routing pages, so according to these two endpoints, it is enough to find the interpolation (intermediate state) during the animation execution process. Fortunately, , these things don’t need to be done by ourselves, Flutter has already done it for us, if you are interested, you can go to the source code related to Hero animation.

interlaced animation

Sometimes we may need some complex animations. These animations may consist of an animation sequence or overlapping animations. For example: there is a histogram that needs to change color while growing in height. After growing to the maximum height, we need in Translate a certain distance on the X axis. It can be found that the above scene contains a variety of animations at different stages. To achieve this effect, it is very simple to use Stagger Animation. Interweaving animation needs to pay attention to the following points:

  1. To create an interleaved animation, multiple animation objects ( ) are used Animation.
  2. One AnimationControllercontrols all animation objects.
  3. Specify a time interval for each animation object ( Interval)

All animations are driven by the same AnimationController, no matter how long the animation needs to last, the value of the controller must be 0.0between 1.0and , and the interval ( Interval) of each animation must be between 0.0and 1.0. For each property that you animate over an interval, you need to create a separate Tweenthat specifies the property's start and end values. In other words, 0.0to 1.0represent the entire animation process, we can specify different start and end points for different animations to determine their start time and end time.

Let's look at an example to realize an animation of the growth of a histogram:

  1. At the beginning, the height grows from 0 to 300 pixels, and the color fades from green to red at the same time; this process takes up 60% of the entire animation time.
  2. After the height grows to 300, start to translate 100 pixels to the right along the X axis; this process takes up 40% of the entire animation time.

We Widgetseparate out the implementation of the animation:

class StaggerAnimation extends StatelessWidget {
    
    

  StaggerAnimation({
    
     Key? key, required this.controller }): super(key: key){
    
    

    var animationFirst = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.0, 0.6, curve: Curves.ease,),//间隔,前60%的动画时间
    );

    var animationAfter = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.6, 1.0, curve: Curves.ease,), //间隔,后40%的动画时间
    );

    // 高度动画
    height = Tween<double>(begin:.0, end: 300.0,).animate(animationFirst);
    // 颜色
    color = ColorTween(begin:Colors.green, end:Colors.red,).animate(animationFirst);
    // 边距
    padding = Tween<EdgeInsets>(
      begin: const EdgeInsets.only(left: .0),
      end: const EdgeInsets.only(left: 100.0),).animate(animationAfter);
  }

  final Animation<double> controller;
  late final Animation<double> height;
  late final Animation<EdgeInsets> padding;
  late final Animation<Color?> color;

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

  
  Widget build(BuildContext context) {
    
    
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

StaggerAnimationThree animations are defined in , which are right Container, height, colorand paddingattribute setting animations, and then Intervalspecify the start point and end point for each animation in the entire animation process by passing. Let's implement the routing to start the animation:

class StaggerRoute extends StatefulWidget {
    
    
  const StaggerRoute({
    
    Key? key}) : super(key: key);

  
  State createState() => _StaggerRouteState();
}

class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
    
    
  late AnimationController _controller;

  
  void initState() {
    
    
    super.initState();
    _controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  }

  Future<void> _playAnimation() async {
    
    
    try {
    
    
      //先正向执行动画
      await _controller.forward().orCancel;
      //再反向执行动画
      await _controller.reverse().orCancel;
    } on TickerCanceled {
    
    
      // the animation got canceled, probably because we were disposed
    }
  }

  
  Widget build(BuildContext context) {
    
    
    return  Scaffold(
      appBar: AppBar(title: const Text("StaggerAnimation"),),
      body: 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),),
            ),
            //调用我们定义的交织动画Widget
            child: StaggerAnimation(controller: _controller),
          ),
        ),
      ),
    );
  }

  
  void dispose() {
    
    
    //路由销毁时需要释放动画资源
    _controller.dispose();
    super.dispose();
  }
}

Execution effect:

insert image description here

animation toggle component

AnimatedSwitcher

In actual development, we often encounter scenarios of switching UI elements, such as tab switching and routing switching. In order to enhance the user experience, an animation is usually specified when switching to make the switching process appear smooth. Some commonly used switching components have been provided in the Flutter SDK component library, such as PageView, , TabViewetc. However, these components cannot cover all demand scenarios. For this reason, a AnimatedSwitchercomponent is provided in the Flutter SDK, which defines a general UI switching abstract.

AnimatedSwitcherShow and hide animations can be added to its new and old child elements at the same time. That is to say, AnimatedSwitcherwhen the sub-element changes, it will animate its old element and new element. Let's first look at AnimatedSwitcherthe definition of :

const AnimatedSwitcher({
    
    
  Key? key,
  this.child,
  required this.duration, // 新child显示动画时长
  this.reverseDuration,// 旧child隐藏的动画时长
  this.switchInCurve = Curves.linear, // 新child显示的动画曲线
  this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
  this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
  this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})

When the AnimatedSwitcherof childchanges (type or Keydifferent), the old childwill execute the hide animation, and the new childwill execute the display animation. What kind of animation effect to perform is transitionBuilderdetermined by the parameter, which accepts a AnimatedSwitcherTransitionBuildertype builder, defined as follows:

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

The new and old bindings will be animated separately when switching builder:AnimatedSwitcherchildchild

  1. For old child, bound animations are executed in reverse ( reverse)
  2. For new child, bound animations will point forward ( forward)

In this way, the new and old animation binding is realized child. AnimatedSwitcherThe default value is AnimatedSwitcher.defaultTransitionBuilder:

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

It can be seen that the object is returned FadeTransition, that is to say, by default, the "fading" and "fading" animations AnimatedSwitcherwill be performed on the old and new .child

example

Let's look at an example below: implement a counter, and then during each self-increment process, the old number performs zoom-out animation and hides, and the new number performs zoom-in animation display. The code is as follows:

import 'package:flutter/material.dart';

class AnimatedSwitcherCounterRoute extends StatefulWidget {
    
    
   const AnimatedSwitcherCounterRoute({
    
    Key key}) : super(key: key); 
   
   State createState() => _AnimatedSwitcherCounterRouteState();
 }

 class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
    
    
   int _count = 0;

   
   Widget build(BuildContext context) {
    
    
     return Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           AnimatedSwitcher(
             duration: const Duration(milliseconds: 500),
             transitionBuilder: (Widget child, Animation<double> animation) {
    
     
               return ScaleTransition(child: child, scale: animation); // 执行缩放动画
             },
             child: Text(
               '$_count',
               // 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
               key: ValueKey<int>(_count),
               style: Theme.of(context).textTheme.headline4,
             ),
           ),
           ElevatedButton(
             child: const Text('+1',),
             onPressed: () {
    
    
               setState(() {
    
    
                 _count += 1;
               });
             },
           ),
         ],
       ),
     );
   }
 }

Run the sample code, when the "+1" button is clicked, the original number will gradually shrink until hidden, while the new number will gradually enlarge, as shown in the figure:

insert image description here

Note: AnimatedSwitcherold and new of , must not be equal childif they are of the same type .Key

AnimatedSwitcher implementation principle

In fact, AnimatedSwitcherthe implementation principle of is relatively simple, and we AnimatedSwitchercan also guess based on the way of use. In order to realize the old and new childswitching animation, only two questions need to be clarified:

  1. When is the animation executed?
  2. How to childanimate old and new?

From AnimatedSwitcherthe way of usage, we can see that when childthere is a change ( if the or type widgetof the child keyis different, it is considered to have changed), it will be re-executed build, and then the animation will start to execute.

We can StatefulWidgetachieve this by inheriting AnimatedSwitcher. The specific method is didUpdateWidgetto judge childwhether its old and new have changed in the callback. If there is a change, perform childa reverse exit ( reverse) animation for the old childand a forward ( forward) entry animation for the new one. The following is AnimatedSwitcherpart of the core pseudocode of the implementation:

Widget _widget; 
void didUpdateWidget(AnimatedSwitcher oldWidget) {
    
    
  super.didUpdateWidget(oldWidget);
  // 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    
    
    // child没变化,...
  } else {
    
    
    //child发生了变化,构建一个Stack来分别给新旧child执行动画
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        //旧child应用FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        //新child应用FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // 给旧child执行反向退场动画
    _controllerOldAnimation.reverse();
    //给新child执行正向入场动画
    _controllerNewAnimation.forward();
  }
}

//build方法
Widget build(BuildContext context){
    
    
  return _widget;
}

The above pseudo-code shows AnimatedSwitcherthe core logic of the implementation. Of course, AnimatedSwitcherthe real implementation is more complicated than this. It can customize the transition animation of entering and exiting the scene and the layout when performing the animation. Here, we cut out the complicated and simplify, and we can clearly see the main implementation ideas through the form of pseudo-code, and the specific implementation can refer to AnimatedSwitcherthe source code.

In addition, a component is also provided in the Flutter SDK AnimatedCrossFade, which can also switch between two sub-elements. The switching process performs a fading animation. The AnimatedSwitcherdifference is that it AnimatedCrossFadeis for two sub-elements , AnimatedSwitcherbut between the old and new values ​​of a sub-element switch . AnimatedCrossFadeThe implementation principle is also relatively simple, AnimatedSwitchersimilar to and, so I won’t go into details, if you are interested, you can check its source code.

Advanced usage of AnimatedSwitcher

Suppose now we want to implement an animation similar to routing translation switching: the old page screen moves to the left to exit, and the new page moves to enter from the right side of the screen. If we want to use AnimatedSwitcherit, we will soon find a problem: it can't be done! We might write the following code:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    
    
    var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransition(
       child: child,
       position: tween.animate(animation),
    );
  },
  ...//省略
)

What's wrong with the code above? We said earlier that the new will be animated forward ( ) and the old will be animated in reverse ( ) when AnimatedSwitcherthe is switched , so the real effect is that the new does pan in from the right side of the screen, but the old does from the Exit on the right side of the screen (not the left). In fact, it is also easy to understand, because without special treatment, the forward and reverse of the same animation are exactly the opposite (symmetrical).childchildforwardchildreversechildchild

So the question is, can it not be used AnimatedSwitcher? The answer is of course no! Think about this problem carefully, the reason is that the same Animationforward ( forward) and reverse ( reverse) are symmetrical. So if we can break this symmetry, then we can realize this function. Next, let’s encapsulate one MySlideTransition. The SlideTransitiononly difference is that the reverse execution of the animation is customized (slide out from the left to hide), the code is as follows:

class MySlideTransition extends AnimatedWidget {
    
    
  const MySlideTransition({
    
    
    Key? key,
    required Animation<Offset> position,
    this.transformHitTests = true,
    required this.child,
  }) : super(key: key, listenable: position);

  final bool transformHitTests;

  final Widget child;

  
  Widget build(BuildContext context) {
    
    
    final position = listenable as Animation<Offset>;
    Offset offset = position.value;
    if (position.status == AnimationStatus.reverse) {
    
    
      offset = Offset(-offset.dx, offset.dy);
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

When calling, SlideTransitionreplace it with MySlideTransition:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    
    
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return MySlideTransition(
      child: child,
      position: tween.animate(animation),
    );
  },
  ...//省略
)

Effect:

insert image description here

It can be seen that in this ingenious way, we have realized an animation similar to the routing entry switch. In fact, the Flutter routing switch is also AnimatedSwitcherrealized through this method.

SlideTransitionX

In the above example, we realized the animation of "left in and right in", so what if we want to realize "left in and right out", "top in and bottom out" or "bottom in and top out"? Of course, we can modify the above code separately, but in this way, each animation has to define a "Transition" separately, which is very troublesome.

The following will encapsulate a general SlideTransitionXto achieve this "in and out animation", the code is as follows:

import 'package:flutter/widgets.dart';

/// 实现同向滑动效果,通常和[AnimatedSwitcher]一起使用
/// Animates the position of a widget relative to its normal position
/// ignoring the animation direction(always slide along one direction).
/// Typically, is used in combination with [AnimatedSwitcher].
class SlideTransitionX extends AnimatedWidget {
    
    
  SlideTransitionX({
    
    
    Key? key,
    required Animation<double> position,
    this.transformHitTests = true,
    this.direction = AxisDirection.down,
    required this.child,
  }) : super(key: key, listenable: position) {
    
    
    _tween = Tween(begin: const Offset(0, 1), end: Offset.zero);
    // 偏移在内部处理
    switch (direction) {
    
    
      case AxisDirection.up:
        _tween.begin = const Offset(0, 1);
        break;
      case AxisDirection.right:
        _tween.begin = const Offset(-1, 0);
        break;
      case AxisDirection.down:
        _tween.begin = const Offset(0, -1);
        break;
      case AxisDirection.left:
        _tween.begin = const Offset(1, 0);
        break;
    }
  }

  final bool transformHitTests;

  final Widget child;

  //退场(出)方向
  final AxisDirection direction;

  late final Tween<Offset> _tween;

  
  Widget build(BuildContext context) {
    
    
    final position = listenable as Animation<double>;
    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;
      }
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

Now if we want to achieve various "sliding in and out animations", it is very easy, just pass directiondifferent direction values, for example, to achieve "top in and bottom out", then:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    
    
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransitionX(
       child: child,
       direction: AxisDirection.down, //上入下出
       position: animation,
     );
  },
  ...//省略其余代码
)

Effect:

insert image description here

You can try to SlideTransitionXtake directiondifferent values ​​​​for to see the running effect.

animation transition component

We collectively refer to the components that perform transition animation when the properties of the Widget change as "animation transition components". The most obvious feature of animation transition components is that they manage themselves internally AnimationController. We know that in order to facilitate the user to customize the animation curve, execution time, direction, etc., in the animation packaging method introduced above, the user usually needs to provide an object to customize these attribute values AnimationController. However, in this way, users must manually manage AnimationController, which will increase the complexity of use. Therefore, if it can also AnimationControllerbe packaged, it will greatly improve the ease of use of animation components.

Custom Animated Transition Components

We want to implement one AnimatedDecoratedBoxthat can decorationperform a transition animation from the old state to the new state when the property changes. Based on what we learned earlier, we implemented a AnimatedDecoratedBox1component:

class AnimatedDecoratedBox extends StatefulWidget {
    
    
  const AnimatedDecoratedBox({
    
    
    Key? key,
    required this.decoration,
    required this.child,
    this.curve = Curves.linear,
    required this.duration,
    this.reverseDuration,
  }) : super(key: key);

  final BoxDecoration decoration;
  final Widget child;
  final Duration duration;
  final Curve curve;
  final Duration? reverseDuration;

  
  State createState() => _AnimatedDecoratedBoxState();
}

class _AnimatedDecoratedBoxState extends State<AnimatedDecoratedBox> with SingleTickerProviderStateMixin {
    
    
  
  AnimationController get controller => _controller;
  late AnimationController _controller;

  Animation<double> get animation => _animation;
  late Animation<double> _animation;

  late DecorationTween _tween;

  
  Widget build(BuildContext context) {
    
    
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
    
    
        return DecoratedBox(
          decoration: _tween.animate(_animation).value,
          child: child,
        );
      },
      child: widget.child,
    );
  }

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

  void _updateCurve() {
    
    
    _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
  }

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

  
  void dispose() {
    
    
    _controller.dispose();
    super.dispose();
  }
}

Let's use it AnimatedDecoratedBoxto achieve the effect that the background color transitions from blue to red after the button is clicked:

class AnimatedDecoratedBoxExample extends StatefulWidget {
    
    
  const AnimatedDecoratedBoxExample({
    
    Key? key}) : super(key: key);
  
  State createState() => _AnimatedDecoratedBoxExampleState();
}
 
class _AnimatedDecoratedBoxExampleState
    extends State<AnimatedDecoratedBoxExample> {
    
    
  Color _decorationColor = Colors.blue;
  var duration = const Duration(seconds: 1);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("自定义动画过渡组件"),
      ),
      body: Center(
        child: AnimatedDecoratedBox(
          duration: duration,
          decoration: BoxDecoration(color: _decorationColor),
          child: TextButton(
            onPressed: () {
    
    
              setState(() {
    
    
                _decorationColor = (_decorationColor == Colors.blue
                    ? Colors.red
                    : Colors.blue);
              });
            },
            child: const Text(
              "AnimatedDecoratedBox",
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

Effect:

insert image description here

Although the above code achieves the functions we expect, the code is more complicated. After a little thought, we can find that AnimationControllerthe management and Tweenupdate part of the code can be abstracted. If we encapsulate these general logics into base classes, then to implement animation transition components, we only need to inherit these base classes and customize ourselves. Different code (such as the construction method of each frame of the animation) is enough, which will simplify the code.

In order to facilitate developers to realize the encapsulation of animation transition components, Flutter provides an ImplicitlyAnimatedWidgetabstract class, which inherits from StatefulWidgetand provides a corresponding ImplicitlyAnimatedWidgetStateclass, and AnimationControllerthe management is in ImplicitlyAnimatedWidgetStatethe class. If developers want to encapsulate animation, they only need to inherit ImplicitlyAnimatedWidgetand ImplicitlyAnimatedWidgetStateclass respectively. Let's demonstrate how to achieve it.

We need to do this in two steps:

  1. Inheritance ImplicitlyAnimatedWidgetclass.
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
    
    
  const AnimatedDecoratedBox({
    
    
    Key? key,
    required this.decoration,
    required this.child,
    Curve curve = Curves.easeIn, //动画曲线
    required Duration duration, // 正向动画执行时长
  }) : super(
          key: key,
          curve: curve,
          duration: duration,
        );
  final BoxDecoration decoration;
  final Widget child;

  
  ImplicitlyAnimatedWidgetState createState() => _AnimatedDecoratedBoxState();
}

Three of these curve、duration、reverseDurationproperties ImplicitlyAnimatedWidgetare defined in . You can see that the class is no different from the class it AnimatedDecoratedBoxnormally inherits from .StatefulWidget

  1. Let Statethe class inherit AnimatedWidgetBaseState(the class inherits from ImplicitlyAnimatedWidgetStatethe class).
class _AnimatedDecoratedBoxState
    extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
    
    
  Tween<dynamic>? _decoration; //定义一个Tween

  
  Widget build(BuildContext context) {
    
    
    return DecoratedBox(
      decoration: _decoration?.evaluate(animation),
      child: widget.child,
    );
  }
 
  
  void forEachTween(TweenVisitor<dynamic> visitor) {
    
    
    // 在需要更新Tween时,基类会调用此方法
    _decoration = visitor(_decoration, widget.decoration,
        (value) => DecorationTween(begin: value));
  }
}

You can see that we have implemented buildand forEachTweentwo methods. During the execution of the animation, buildthe method will be called every frame (the calling logic is ImplicitlyAnimatedWidgetStatein), so buildwe need to build DecoratedBoxthe state of each frame in the method, so we have to calculate decorationthe state of each frame, which we can _decoration.evaluate(animation)calculate by , where animationis ImplicitlyAnimatedWidgetStateThe object defined in the base class _decorationis a type of object that we customize DecorationTween, so the question now is when is it assigned?

To answer this question, we have to figure out when we need to assign _decorationa value. We know _decorationthat it is one Tween, and Tweenthe main responsibility is to define the start state ( begin) and end state ( end) of the animation. For AnimatedDecoratedBox, decorationthe final state is the value passed to it by the user, and the initial state is uncertain. There are two situations:

  1. AnimatedDecoratedBoxFor the first time build, its value is directly decorationset to the initial state at this time, that is, _decorationthe value is DecorationTween(begin: decoration).
  2. AnimatedDecoratedBoxWhen the update of decoration, the initial state is _decoration.animate(animation), that is, _decorationthe value is DecorationTween(begin: _decoration.animate(animation),end:decoration).

Now forEachTweenthe function is obvious, it is used to update Tweenthe initial value, it will be called in the above two cases, and the developer only needs to rewrite this method, and update the Tweeninitial state in this method value. And some update logic is shielded in visitorthe callback, we only need to call it and pass the correct parameters to it, visitorthe method signature is as follows:

 Tween<T> visitor(
   Tween<T> tween, //当前的tween,第一次调用为null
   T targetValue, // 终止状态
   TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
 );

It can be seen that the encapsulation of animation transition components can be quickly realized through inheritance ImplicitlyAnimatedWidgetand classes, which simplifies the code a lot compared with our pure manual implementation.ImplicitlyAnimatedWidgetState

Flutter's preset animation transition components

There are also many animation transition components preset in the Flutter SDK, and the implementation methods are AnimatedDecoratedBoxsimilar to most of them, as shown in the following table:

component name Function
AnimatedPadding paddingTransition animations are performed to the new state when a change occurs
AnimatedPositioned Used Stacktogether, when the positioning state changes, the transition animation will be performed to the new state
AnimatedOpacity Perform transition animation to new state when transparency opacitychanges
AnimatedAlign When alignmenta change occurs, a transition animation is performed to the new state
AnimatedContainer When Containerthe property changes, the transition animation will be performed to the new state
AnimatedDefaultTextStyle When the font style changes, the text component that inherits the style in the child component will dynamically transition to the new style

Let's use an example to feel the effects of these preset animation transition components:

import 'package:flutter/material.dart';

class AnimatedWidgetsTest extends StatefulWidget {
    
    
  const AnimatedWidgetsTest({
    
    Key? key}) : super(key: key);

  
  State createState() => _AnimatedWidgetsTestState();
}

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

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

running result:

insert image description here

Animation source code analysis

AnimationThe key classes and their relationships are shown in Figure 8-5.

insert image description here

In Figure 8-5, Animationit is the key class of animation, which inherits from and Listenableholds Animationan animation value ( value) and an animation state ( status) for external monitoring. External users will update the UI and other operations based on these monitoring callbacks.

AnimationControllerIs Animationthe most common implementation, it holds two key fields - Tickerand Simulation, the former is TickerProviderprovided by the interface, used to provide the "heartbeat" to drive animation updates, mainly used for tween animation; the latter provides update rules for animation values, Implemented by concrete subclasses.

SimulationThe default implementation of is _InterpolationSimulationthat it Curvecomputes values ​​based on concrete subclasses of . In addition, Simulationit also provides the ability to simulate various physical effects, such as SpringSimulationthe simulation of spring effects.

Generally speaking, the field AnimationControllerof valueis not used directly, _AnimatedEvaluationit will hold an Animationobject (usually an AnimationControllerinstance of ) and an Animatableobject, the former provides the original value of the animation, and the latter uses the animation value of the former ( value) as a parameter for "tweening ” ( Tween), the final value will be exposed externally through the field _AnimatedEvaluationof the instance .value

The following is a more specific analysis from the perspective of source code.

motion tween

Tween animation is only interpolated according to a certain rule within a specified time range, and its usage process is usually shown in Listing 8-28.

// 代码清单8-28 补间动画示例
AnimationController animationController = // 见代码清单8-29
         AnimationController(vsync: this, duration: Duration(milliseconds: 1000));
Animation animation =  // 见代码清单8-30
         Tween(begin: 0.0,end: 10.0).animate(animationController);
animationController.addListener(() {
    
     // 通知发生,见代码清单8-40
 setState(() {
    
     newValue = animation.value });
});
animationController.forward(); // 见代码清单8-32

In the above logic, AnimationControllerit is the driver that Tweenprovides the interpolation model of the tween animation Animationas the final call exit. The following is an in-depth analysis of the code.

First analyze AnimationControllerthe initialization logic, as shown in Listing 8-29.

// 代码清单8-29 flutter/packages/flutter/lib/src/animation/animation_controller.dart
AnimationController({
    
      ......, required TickerProvider vsync,
    }) : _direction = _AnimationDirection.forward {
    
    
  _ticker = vsync.createTicker(_tick); // _tick将在"心跳"时触发,见代码清单8-38
  _internalSetValue(value ?? lowerBound); // 更新当前动画的状态
}
void _internalSetValue(double newValue) {
    
    
  _value = newValue.clamp(lowerBound, upperBound);
  if (_value == lowerBound) {
    
    
    _status = AnimationStatus.dismissed; // 动画尚未开始
  } else if (_value == upperBound) {
    
    
    _status = AnimationStatus.completed; // 已经结束
  } else {
    
    
    _status = (_direction == _AnimationDirection.forward) ? // 动画进行中
            AnimationStatus.forward : AnimationStatus.reverse;
  }
}  

AnimationControllerIn the initialization logic of , first create an Tickerobject, which is the core driver of motion tween animation, which will be analyzed in detail later. _internalSetValueResponsible for updating the state of the current animation, so I won't go into details here.

In Code Listing 8-28, animation.valuethe current value will be obtained when each frame of animation is updated, and the calculation process is shown in Code Listing 8-30.

// 代码清单8-30 flutter/packages/flutter/lib/src/animation/tween.dart 
abstract class Animatable<T> {
    
    
  T transform(double t); // 见代码清单8-31
  T evaluate(Animation<double> animation) => transform(animation.value);
  Animation<T> animate(Animation<double> parent) {
    
    
    return _AnimatedEvaluation<T>(parent, this);
  }
} // Animatable
class _AnimatedEvaluation<T> extends Animation<T>
             with AnimationWithParentMixin<double> {
    
    
  _AnimatedEvaluation(this.parent, this._evaluatable);
  
  final Animation<double> parent; // 通常为AnimationController 
  final Animatable<T> _evaluatable; // 见代码清单8-31
    // 代码清单8-28中获取的值
  T get value => _evaluatable.evaluate(parent);  // 见前面内容
}

TweenIt is Animatablea subclass whose animatemethod is implemented by the parent class and mainly returns _AnimatedEvaluationthe object, so animation.valuewhen calling, the essence is to call the specific interpolation model, that is, the method of Animatablethe specific subclass (such as ) , but in fact it calls the method, as an example, its The logic is shown in Listing 8-31.TweenevaluateevaluatetransformTween

// 代码清单8-31 flutter/packages/flutter/lib/src/animation/tween.dart
class Tween<T extends dynamic> extends Animatable<T> {
    
    
  Tween({
    
    this.begin, this.end,});
  
  T lerp(double t) {
    
     // Linear Interpolation
    return begin + (end - begin) * t as T;
  }
  
  T transform(double t) {
    
     // t 即AnimationController的值,在代码清单8-33中进行定义
    if (t == 0.0) return begin as T;
    if (t == 1.0) return end as T;
    return lerp(t); // 线性差值的逻辑
  }
}

The above logic is a typical linear interpolation process. The parameter is AnimationController.valuethat the value will be updated every frame, and Tweenthe linear interpolation will be completed based on this value, and the result will be _AnimatedEvaluationthe value of the object.

Next, analyze AnimationController.valuethe updated driving mechanism, that is, forwardthe method, as shown in Listing 8-32.

// 代码清单8-32 flutter/packages/flutter/lib/src/animation/animation_controller.dart
TickerFuture forward({
    
     double? from }) {
    
     // AnimationController
  _direction = _AnimationDirection.forward;
  if (from != null) value = from;
  return _animateToInternal(upperBound);
}
TickerFuture _animateToInternal(double target, // 即upperBound
    {
    
     Duration? duration, Curve curve = Curves.linear }) {
    
    
  double scale = 1.0;
  if (SemanticsBinding.instance!.disableAnimations) {
    
     ...... } // SKIP 禁用动画的逻辑
  Duration? simulationDuration = duration; // 第1步,计算动画的执行时长
  if (simulationDuration == null) {
    
     // 没有指定动画时长
    final double range = upperBound - lowerBound; // 根据当前进度,以1s为基准计算
    final double remainingFraction = range.isFinite ? (target - _value).abs() / 
       range : 1.0;
    final Duration directionDuration = // 根据动画是顺序还是逆序来计算最终的执行时长
      (_direction == _AnimationDirection.reverse && reverseDuration != null)
      ? reverseDuration! : this.duration!;
    simulationDuration = directionDuration * remainingFraction;
  } else if (target == value) {
    
     // 动画已完成,对应第2步
    simulationDuration = Duration.zero;
  }
  stop(); // 见代码清单8-41
  if (simulationDuration == Duration.zero) {
    
     // 第2步,动画结束,完成字段更新,并通知
    if (value != target) {
    
    
      _value = target.clamp(lowerBound, upperBound); // 修正值到合法的目标值
      notifyListeners(); // 通知值变化,见代码清单8-40
    }
    _status = (_direction == _AnimationDirection.forward) ?
           AnimationStatus.completed : AnimationStatus.dismissed;
    _checkStatusChanged(); // 更新动画状态,见代码清单8-40
    return TickerFuture.complete();
  } // if
  return _startSimulation(_InterpolationSimulation( // 第3步,开始动画
          _value, target, simulationDuration, curve, scale));
}

The above logic is mainly divided into 3 steps. The first step is simulationDurationthe calculation, which will be calculated based on the ratio of the remaining value to the total value, stopand the logic will be introduced later. The second step is mainly to deal simulationDurationwith 0the situation that the animation has ended, and at this time, the assignment of the field and the notification of the state are performed. The third step is to actually start the animation. Note that Simulationthe specific implementation class here is _InterpolationSimulationthat it is a linear interpolation simulator, which will be analyzed in detail later. The logic of the first analysis _startSimulationis shown in Listing 8-33.

// 代码清单8-33 flutter/packages/flutter/lib/src/animation/animation_controller.dart

double get value => _value;
TickerFuture _startSimulation(Simulation simulation) {
    
     // AnimationController
  _simulation = simulation;
  _lastElapsedDuration = Duration.zero; // 截止到上一帧动画,已消耗的时间
  _value = simulation.x(0.0).clamp(lowerBound, upperBound); 
// 起始值,x方法见代码清单8-39
  final TickerFuture result = _ticker!.start(); // 开始请求"心跳",驱动动画
  _status = (_direction == _AnimationDirection.forward) ?
                AnimationStatus.forward : AnimationStatus.reverse;
  _checkStatusChanged();
  return result;
}

The above logic uses _ticker!.starthe t method to start the animation and update _statusthe state at the same time. startThe logic of the method is shown in Listing 8-34.

// 代码清单8-34 flutter/packages/flutter/lib/src/scheduler/ticker.dart
TickerFuture start() {
    
    
  _future = TickerFuture._(); // 表示一个待完成的动画
  if (shouldScheduleTick) {
    
     scheduleTick(); } // 请求"心跳"
  if (SchedulerBinding.instance!.schedulerPhase.index
          > SchedulerPhase.idle.index
      && SchedulerBinding.instance!.schedulerPhase.index
          < SchedulerPhase.postFrameCallbacks.index) // 在这个阶段内应修正动画开始时间
    _startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
  return _future!;
}

The above logic is mainly to scheduleTickinitiate a "heartbeat" through the method, and there is one detail that needs attention: if a frame is currently being processed, it _startTimewill be counted from Vsyncthe arrival time of the signal of the current frame. This is a small correction, namely If a frame is already being rendered when the animation starts, the animation state for the next frame should be equivalent to the state of the animation two frames later. scheduleTickThe logic of the method is shown in Listing 8-35.

// 代码清单8-35 flutter/packages/flutter/lib/src/scheduler/ticker.dart
bool get muted => _muted; // 是否为静默状态
bool _muted = false;
bool get isActive => _future != null; // 存在动画

bool get scheduled => _animationId != null; // 已经在等待"心跳"
 // 调用逻辑见代码清单8-37,用于判断是否需要"心跳"
bool get shouldScheduleTick => !muted && isActive && !scheduled;

void scheduleTick({
    
     bool rescheduling = false }) {
    
    
  _animationId = SchedulerBinding.instance!.scheduleFrameCallback(
                      _tick, rescheduling: rescheduling); // 见代码清单8-36
}

In the above logic, the functions of several key attribute fields have been marked, and scheduleFrameCallbackthe method will _tickregister the function in the callback list when the next Vsyncsignal arrives, as shown in Listing 8-36.

// 代码清单8-36 flutter/packages/flutter/lib/src/scheduler/ticker.dart
int scheduleFrameCallback(FrameCallback callback, {
    
     bool rescheduling = false }) {
    
    
  scheduleFrame(); // 见代码清单5-20
  _nextFrameCallbackId += 1;
  _transientCallbacks[_nextFrameCallbackId] // 处理逻辑见代码清单5-36
      = _FrameCallbackEntry(callback, rescheduling: rescheduling);
  return _nextFrameCallbackId;
}

_transientCallbackscallbackIts callback , that is, the method, will be processed after the Vsync signal arrives _tick, as shown in Listing 8-37.

// 代码清单8-37 flutter/packages/flutter/lib/src/scheduler/ticker.dart
void _tick(Duration timeStamp) {
    
     // Ticker
  _animationId = null;
  _startTime ??= timeStamp; // 首次"心跳"的时间戳,timeStamp是当次"心跳"的时间戳
  // 注意这里的 ??= 语法表明只会记录首次"心跳"发生时的时间戳
  _onTick(timeStamp - _startTime!); // 见代码清单8-38,参数为动画已经执行的时间
  if (shouldScheduleTick) scheduleTick(rescheduling: true); 
// 如有必要,等待下一次"心跳"
}

So far, it can be said with certainty that the so-called "heartbeat" ( tick) is actually Vsynca signal, _startTimewhich will only be assigned once, indicating the timestamp of the start of the animation, and then call _onTickthe method with the time difference as a parameter (that is, the parameter of the previous content - _tickfunction), if the logic shouldScheduleTickAfter completion true, it will continue to register Vsyncthe signal to drive the generation of the next "heartbeat". The logic of the analysis method below _tickis shown in Listing 8-38.

// 代码清单8-38 flutter/packages/flutter/lib/src/animation/animation_controller.dart
// 该方法的注册逻辑位于代码清单8-29的createTicker方法中
void _tick(Duration elapsed) {
    
     // AnimationController
  _lastElapsedDuration = elapsed; // 动画已经执行的时间
  final double elapsedInSeconds = // 毫秒转秒
    elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
  assert(elapsedInSeconds >= 0.0);
  _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound); // 插值
  if (_simulation!.isDone(elapsedInSeconds)) {
    
     // 判断是否完成,见代码清单8-39
    _status = (_direction == _AnimationDirection.forward) ? // 完成则更新状态
                AnimationStatus.completed :AnimationStatus.dismissed;
    stop(canceled: false); // 停止动画,见代码清单8-41
  } // 否则,代码清单8-37中的shouldScheduleTick为true,将继续等待"心跳"(请求Vsync)
  notifyListeners(); // 见代码清单8-40
  _checkStatusChanged();
}

The above logic first calculates the duration of the animation that has been executed in seconds, then calls _simulationthe xmethod to calculate the current value, and finally judges whether the animation is complete, and broadcasts _valuethe changes of its own fields.

First analyze xthe method, Tweenanimation defaults to _InterpolationSimulation, as shown in Listing 8-39.

// 代码清单8-39 flutter/packages/flutter/lib/src/animation/animation_controller.dart
 // _InterpolationSimulation
double x(double timeInSeconds) {
    
    
  final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
  if (t == 0.0) return _begin;
  else if (t == 1.0) return _end;
  else return _begin + (_end - _begin) * _curve.transform(t);
}
 // 是否完成完全取决于动画执行的时间,注意与代码清单8-44中介绍的物理动画进行区分
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;

In the above logic, _curvethe default is Curves.linearto return titself, so the return value is ta linear function, and the coefficient is ( _end - _begin).

Secondly, analyze _valuethe notification logic after the calculation is completed, as shown in the code list 8-40.

// 代码清单8-40 flutter/packages/flutter/lib/src/animation/animation_controller.dart
void notifyListeners() {
    
    
  final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
  for (final VoidCallback listener in localListeners) {
    
    
    InformationCollector? collector;
    try {
    
    
      if (_listeners.contains(listener)) listener(); // 通知监听器
    } catch (exception, stack) {
    
     ...... }
  }
}
void _checkStatusChanged() {
    
    
  final AnimationStatus newStatus = status;
  if (_lastReportedStatus != newStatus) {
    
     // 状态发生改变才需要通知
    _lastReportedStatus = newStatus;
    notifyStatusListeners(newStatus);
  }
}

In the above logic, notifyListenersthe method is responsible for _valuethe change of the notification, which is basically called every frame; notifyStatusListenersit is responsible for the change of the notification state, and it will only be triggered when the animation state changes.

The logic of the final analysis isDone. For _InterpolationSimulationit, its logic is shown in code list 8-39, that is, it stops when the execution time exceeds the target time. This is also the characteristic of tween animation, that is, time is the determining factor of the "heartbeat" of animation, and users can only customize specific interpolation rules.

Let's continue to analyze stophow the method stops the animation, as shown in Listing 8-41.

// 代码清单8-41 flutter/packages/flutter/lib/src/animation/animation_controller.dart
void stop({
    
     bool canceled = false }) {
    
    
  if (!isActive) return;
  final TickerFuture localFuture = _future!; 
  _future = null;
  _startTime = null;
  unscheduleTick(); // 停止等待"心跳"
  if (canceled) {
    
    
    localFuture._cancel(this);
  } else {
    
    
    localFuture._complete();
  }
}

The above logic is mainly to reset animation-related fields, so I won’t go into details here.

In general, developers can also drive the execution of the next frame buildthrough the method in the method , so as to achieve the effect of animation, but it provides a flexible, scalable, and easy-to-manage resource framework, which saves a lot of resources for developers. energy.Future+setStateAnimation

Summarize:

The essence of tween animation is to register Vsyncthe signal callback to the bottom layer:

  • ticker.startAfter the method starts the animation, call scheduleTick--> call SchedulerBinding.instance!.scheduleFrameCallback--> call scheduleFrame()to go through the drawing process,

  • And in FrameCallbackwill execute _tickthe method, which onTickwill call notifyListeners()the notification animation to monitor and call back valuethe change of the current animation, which will be called basically every frame; notifyStatusListenersit is responsible for the change of the notification state, which will only be triggered when the animation state changes.

Supplement: scheduleFrameThe logic of the method is shown in Code Listing 5-20. It will initiate a request to the Engine through the interface, requesting rendering when window.scheduleFramethe next signal arrives.Vsync

// 代码清单5-20 flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleFrame() {
    
    
  if (_hasScheduledFrame || !framesEnabled) return;
  ensureFrameCallbacksRegistered();  
  window.scheduleFrame(); 
  _hasScheduledFrame = true;
}

physical animation

Although tween animation is flexible, its time is often fixed, which is not suitable for some scenes. Take Figure 8-6 as an example. When the user drags the box away from the center point, if you want the box to return to its original position with a spring-dragged animation effect, it is difficult to realize the tween animation, because the completion time of the animation depends on the user's sliding distance. Velocity and various properties of the spring. At this point you need to use Physics (Physics) animation.

insert image description here

Code List 8-42 is the key implementation code for the animation effect in Figure 8-6.

// 代码清单8-42 物理动画示例
void _runAnimation(Offset pixelsPerSecond,  Size size) {
    
    
  // pixelsPerSecond表示拖曳手势结束、动画开始时
  // 方块由于拖曳手势而在X、Y方向上因物理惯性而产生的移动速度
  _animation = _controller.drive( // 触发AlignmentTween的animate方法
    AlignmentTween(begin: _dragAlignment, end: Alignment.center,));
  // 由6.2节可知,由于Alignment的坐标系是[0,1]形式的,因此需要除以屏幕大小,转为比例
  final unitsPerSecondX = pixelsPerSecond.dx / size.width;
  final unitsPerSecondY = pixelsPerSecond.dy / size.height;
  final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
  final unitVelocity = unitsPerSecond.distance;
  const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1,); 
	// 弹簧的各种属性
  final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); 
	// 构造一个弹簧物理模型
  _controller.animateWith(simulation);
}

void initState() {
    
    
  super.initState();
  _controller = AnimationController(vsync: this);
  _controller.addListener(() {
    
    
    setState(() {
    
     _dragAlignment = _animation.value;});
  });
}

In the above logic, pixelsPerSecondit is a parameter carried by the callback of the dragging gesture, indicating the current user's sliding speed, that is, the speed at which the box is pulled out. _dragAlignmentRepresents the real-time position of the block, so when _runAnimationthe method starts executing, its value is exactly the real position of the animation. At this point, we know where the cube starts and ends, and the velocity at which the drag gesture ends. Next is the construction of the physical model of the sliding process, mainly the calculated SpringSimulationparameters, unitVelocitythat is, the sliding speed of the spring in the linear direction, opposite to the direction of the spring.

The following direct analysis animateWithmethod, as shown in the code list 8-43.

// 代码清单8-43 flutter/packages/flutter/lib/src/animation/animation_controller.dart
TickerFuture animateWith(Simulation simulation) {
    
    
  stop(); // 见代码清单8-41
  _direction = _AnimationDirection.forward;
  return _startSimulation(simulation); // 见代码清单8-33
}

Since _startSimulationthe previous content of the logic has been analyzed, the method Simulationof the xmethod can be directly analyzed isDone, as shown in the code list 8-44.

// 代码清单8-44 flutter/packages/flutter/lib/src/physics/spring_simulation.dart
class SpringSimulation extends Simulation {
    
    
  final _SpringSolution _solution; // 弹簧物理模型的抽象表示
  
  double x(double time) => _endPosition + _solution.x(time);
  
  double dx(double time) => _solution.dx(time);
  
  bool isDone(double time) {
    
    
    return nearZero(_solution.x(time), tolerance.distance) &&
           nearZero(_solution.dx(time), tolerance.velocity);
  }
}

From the above logic, we can see SpringSimulationthat the main logic of the field is handed over to _solutionthe field, which is created by the code in Listing 8-42 SpringDescription, and the physical model of the spring will not be specifically analyzed here. SpringSimulationThe isDonemethod is also relatively easy to understand, that is, to judge whether the current position has reached the target position. This is also a key difference between tween animation and physical animation - isDonethe realization of the method: judge whether to end according to whether the physical position meets the condition rather than the time factor.


reference:

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/130913731