1. Ticker

Why Ticker first? When doing animation, we define AnimationController, must pass vsync parameters, usually the parameters for this, the current State, mixed with SingleTickerProviderStateMixin provide (a mixin). From the name of the mixin class, we can see that its main purpose is to provide a Ticker. So what is a Ticker?

class Ticker { Ticker(this._onTick, { this.debugLabel }) { ... }... // source not such a bool muted; bool isTicking; bool isActive; bool isicking; . TickerFuture start() { ... if (shouldScheduleTick) { scheduleTick(); }... } void stop({ bool canceled = false }) { ... } void _tick(Duration timeStamp) { ... } @protected bool get shouldScheduleTick => ! muted && isActive && ! scheduled; @protectedvoid scheduleTick({ bool rescheduling = false }) { _animationId = SchedulerBinding.instance! .scheduleFrameCallback(_tick, rescheduling: rescheduling); } void _tick(Duration timeStamp) { assert(isTicking); assert(scheduled); _animationId = null; _startTime ?? = timeStamp; _onTick(timeStamp - _startTime!) ; // The onTick callback may have scheduled another tick already, for // example by calling stop then start again. if (shouldScheduleTick) scheduleTick(rescheduling: true); }... }Copy the code

Ticker’s source code is relatively simple, with some major code inserted here. You can see that the Ticker is like a ticking clock. You can start, you can stop, you have your own state.

Seeing it always reminds me of a clock in my house when I was a kid. I could hear it ticking every night when I went to bed…

Take a look at how Ticker works: the start method calls shceduleTick, which schedules a frame to the system using a SchedulerBinding object and adds the _tick callback. Note that scheduleTick is also called based on the condition in _tick to create a loop. When the conditions are not met, the system stops scheduling.

2. AnimationController

The AnimationController is the object that we control the animation directly. Analyze the main fields and methods.

class AnimationController extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { AnimationController({ double? Value, this.duration, this.reverseDuration, this.debugLabel, this.lowerBound = 0.0, this.upperBound = 1.0, this.animationBehavior = AnimationBehavior.normal, required TickerProvider vsync, }) : assert(lowerBound ! = null), assert(upperBound ! = null), assert(upperBound >= lowerBound), assert(vsync ! = null), _direction = _AnimationDirection.forward { _ticker = vsync.createTicker(_tick); _internalSetValue(value ?? lowerBound); }... @overrideAnimationStatus get status => _status; late AnimationStatus _status; TickerFuture forward({ double? from }) { _direction = _AnimationDirection.forward; if (from ! = null) value = from; return _animateToInternal(upperBound); } void reset() { value = lowerBound; } void stop({ bool canceled = true }) { _simulation = null; _lastElapsedDuration = null; _ticker! .stop(canceled: canceled); }... }Copy the code

You can see that the AnimationController constructor uses the vsync object in the initialization list to create _ticker. The animation starts the forward method and eventually calls the _startSimulation method, which starts the _ticker. The stop method stops the animation with the _ticker.stop method. Notice how the Animation value _value changes between Animation startup _animateToInternal and Animation execution _tick.

You can see that the AnimationController performs the animation by scheduling the system request frames with the Ticker. During the execution of the animation, the current animation value _value is calculated, and the animation is updated with the current value _value.

3. Explicit Animations

When introducing animations, the Flutter framework introduces two basic animations, Explicit animations and Implicit animations, according to different user needs. A simple way to tell which of these two animations we should use in a particular scene is, “Do you need to interfere with the animation?” For example, when the animation is finished you need to repeat the animation, flip the animation, stop the animation in the middle of execution, etc. Choose Explicit Animations if you need to intervene with animations; otherwise, choose Explicit Animations.

The Flutter framework has prepared some explicit animations for us, usually named XxxTransition, inherited from the AnimatedWidget.

AnimatedWidget

We talked about Ticker, AnimationController. We have a clock, we have a value, so how is that value reflected in the animation?

AnimatedWidget

abstract class AnimatedWidget extends StatefulWidget { const AnimatedWidget({ Key key, @required this.listenable, }) : assert(listenable ! = null), super(key: key); final Listenable listenable; . @overrid _AnimatedState createState() => _AnimatedState(); . }Copy the code

_AnimatedState

class _AnimatedState extends State<AnimatedWidget> { @override void initState() { super.initState(); widget.listenable.addListener(_handleChange); }... void _handleChange() { setState(() { // The listenable's state is our build state, and it changed already. }); }... }Copy the code

When we construct XxxTransition, we need to pass an Animation object. This is a Listenable (usually our AnimationController),

Hand in a parent class (AnimatedWidget). As you can see from the code for _AnimatedState, the system has added a listening callback to update the state.

So where do we send the notification that the Widget needs to be updated? It’s easy to imagine that the animation value _value has changed and the page needs to be refreshed to perform the animation. So the AnimationController:

AnimationController

class AnimationController ... {... void _tick(Duration elapsed) { _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)) { _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.completed : AnimationStatus.dismissed; stop(canceled: false); } // Notice notice (); _checkStatusChanged(); }... }Copy the code

You can see that every time _value is computed in the _tick, a notification is sent and the animation status is checked.

Of course, this is not the only place to send notifications to update animations. The rest of the place, readers can check.

AnimatedBuilder

AnimatedBuilder by the way. AnimatedBuilder also inherits from AnimatedWidget. We can use AnimatedBuilder to customize our animations when the XxxTransition provided to us by the system does not meet our needs. Here we need to deal with the properties we want to modify based on the _value of the animation.

Note the second parameter to the Builder, child, which can be used for optimization when the animation does not involve modifying the internal child widgets.

4. Implicit Animations

When we only need a “one-time” animation and don’t need an AnimationController to control the animation, we can use implicit Animation.

Similar to explicit animation, Flutter provides us with AnimatedXxx, which is used as implicit animations.

With this knowledge behind them, implicit animations are simple. Let’s focus on AnimationController and State.

AnimatedXxx is a StatefulWidget, so let’s focus on its corresponding State.

abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends ImplicitlyAnimatedWidgetState<T> { @override void initState() { super.initState(); controller.addListener(_handleAnimationChanged); } void _handleAnimationChanged() { setState(() { /* The animation ticked. Rebuild with new animation value */ }); }}Copy the code

You can see that in the AnimatedWidgetBase Estate, there’s a listener doing a status update.

abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> { @protected AnimationController get controller => _controller; AnimationController _controller; . @overridevoid initState() { super.initState(); _controller = AnimationController( duration: widget.duration, debugLabel: kDebugMode ? widget.toStringShort() : null, vsync: this, ); . }}Copy the code

In ImplicitlyAnimatedWidgetState AnimationController initialization.

5. You have to Tween

As mentioned above, the animation process evaluates the _value each time and then sends a notification to update the view. But it doesn’t say how the value of _value is evaluated. So obviously the Tween here has to do with calculating values.

class Tween<T extends dynamic> extends Animatable<T> { Tween({ this.begin, this.end, }); T? begin; T? end; @protected T lerp(double t) { assert(begin ! = null); assert(end ! = null); return begin + (end - begin) * t as T; } @override T transform(double T) {if (T == 0.0) return begin as T; If (t == 1.0) return as t; return lerp(t); }}Copy the code

The source code is very simple, a starting value, an end value, two methods to get the value.

You might be wondering, why do I have to do this?

Because it’s not as easy as it looks.

We usually think of computation as numerical addition, subtraction, multiplication and division, so what if I want to change colors dynamically? The shape? The Flutter framework already provides us with a calculation type for these property changes.

I tagged two random Tween’s that might be used.

Let’s take a look at the use of Tween in Animations with Implicit animations. For the use of the explicit animations Tween, you can consult the source code.

Implicit Animation Tween’s app

When we use Implicit Animation AnimatedXxx, we don’t seem to tell it how to change, so how does it change?

So let’s look at _AnimatedPaddingState

class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> { EdgeInsetsGeometryTween _padding; @overridevoid forEachTween(TweenVisitor<dynamic> visitor) { _padding = visitor(_padding, widget.padding, (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween; } @override Widget build(BuildContext context) { return Padding( padding: _padding .evaluate(animation) .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity), child: widget.child, ); }}Copy the code

So forEachTween, forEachTween has a parameter visitor that receives 3 parameters, Tween Tween, T value, Tween Function(T value). So what this method does is basically construct a tween based on value or update a tween, which is a property in State.

As you can see in the build method, when the padding is used, the system calculates the padding value based on the current animation.

Curve 

If you say “Tween”, you can say “Curve”. Curve is a class that describes the rate at which an animation changes. See this link for some Curve effects.

Curves

conclusion

The Ticker is the “clock” that drives the animation. The AnimationController wrapper hides operations on the Ticker, and we use the Controller to control the Ticker for animation execution. Tween’s job is to calculate the value of the Animation of a particular Animation type based on the current time, and then update the Widget properties to animate it.