In the previous article, we introduced the use of animation in detail. In this article, we will analyze the underlying logic of animation from the source point of view.

The implementation mechanism of animation

Animation control is implemented by the AnimationController, so we will start with the AnimationController.

  • AnimationController constructor
<! -- AnimationController --> AnimationController({ double? Value, this.duration, this.reverseDuration, this.debugLabel, this.lowerBound = 0.0, this.upperBound = 1.0, this.animationBehavior = AnimationBehavior.normal, required TickerProvider vsync, }) : _direction = _AnimationDirection.forward { _ticker = vsync.createTicker(_tick); _internalSetValue(value ?? lowerBound); } void _tick(Duration Elapsed) {// What is elapsed? } Ticker? _ticker;Copy the code

The AnimationController constructor initializes the _ticker property with the vsync.createticker (_tick) method and sets the initial value for the animation.

An important function of the _ticker is to hold the callback void _tick(Duration Elapsed), which we will explain in more detail later.

  • AnimationControllertheforwardmethods
<! -- AnimationController --> TickerFuture forward({ double? from }) { _direction = _AnimationDirection.forward; if (from ! = null) value = from; return _animateToInternal(upperBound); } TickerFuture _animateToInternal(double target, { Duration? duration, Curve curve = Curves.linear }) { stop(); // omit content... return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale)); } TickerFuture _startSimulation(Simulation simulation) { // start final TickerFuture result = _ticker! .start(); return result; } void stop({ bool canceled = true }) { // stop _ticker! .stop(canceled: canceled); }Copy the code

The forward method calls _ticker first! Stop (Canceled: true) and called _ticker! Start () method.

Both methods mentioned above are _ticker methods, so what do they do?

  • Ticker
<! -- Ticker --> void cancel ({bool canceled = false}) {// Cancel canceled Tick unscheduleTick(); } TickerFuture start() {// omit content... If (shouldScheduleTick) {// 1 start scheduleTick(); } if (SchedulerBinding.instance! .schedulerPhase.index > SchedulerPhase.idle.index && SchedulerBinding.instance! . SchedulerPhase. Index < schedulerPhase. PostFrameCallbacks. Index) / / 2 records _startTime = SchedulerBinding animation start time. The instance! .currentFrameTimeStamp; return _future! ; }Copy the code
  1. stopMethod calledunscheduleTickMethods to cancelTick scheduling;
  2. startMethod calledscheduleTickMethods toTick schedulingAnd recorded the start time.

What does Tick scheduling mean?

<! -- Ticker --> void scheduleTick({ bool rescheduling = false }) { _animationId = SchedulerBinding.instance! .scheduleFrameCallback(_tick, rescheduling: rescheduling); } void unscheduleTick() { if (scheduled) { SchedulerBinding.instance! .cancelFrameCallbackWithId(_animationId!) ; _animationId = null; }}Copy the code
<! -- SchedulerBinding --> int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) { scheduleFrame(); _nextFrameCallbackId += 1; _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling); return _nextFrameCallbackId; } void cancelFrameCallbackWithId(int id) { _transientCallbacks.remove(id); _removedIds.add(id); }Copy the code
  1. scheduleTickIs toAnimationControllerthe_tickAdded to theSchedulerBindingthe_transientCallbacksArray, and then returns a correspondingThe callback ID, and then asks to refresh the screen.
  2. unscheduleTickIs based onThe callback IDwillAnimationControllerthe_tickfromSchedulerBindingthe_transientCallbacksArray.
  • SchedulerBinding

The familiar SchedulerBinding is now in place, and its functionality and call logic should be familiar to you if you’ve read the previous articles.

void handleBeginFrame(Duration? Public adjustForepoch (public adjustforepoch) {public adjustforech (public adjustforech?? _lastRawTimeStamp); if (rawTimeStamp ! = null) _lastRawTimeStamp = rawTimeStamp; _hasScheduledFrame = false; try { final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks; _transientCallbacks = <int, _FrameCallbackEntry>{}; callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { if (! Contains (id)) // 2 callback _invokeFrameCallback(callBackentry.callback, _currentFrameTimeStamp! , callbackEntry.debugStack); }); _removedIds.clear(); } finally { } }Copy the code

Every time a page is refreshed, the Flutter Engine calls back to the handleBeginFrame method of the SchedulerBinding and passes a timestamp. The callback function in the _transientCallbacks array is called one by one and the timestamp is passed in.

HandleBeginFrame is called before the refresh interface function _handleDrawFrame, from which we can see that the handleBeginFrame is mainly used to process and set the intermediate value of the animation before drawing, so that it can be easily redrawn.

Let’s go back to the _tick logic of the AnimationController.

  • _tickmethods
void _tick(Duration elapsed) { _lastElapsedDuration = elapsed; / / 1. The time of computing has animation final double elapsedInSeconds = elapsed. InMicroseconds. ToDouble ()/Duration. MicrosecondsPerSecond; // 2. Calculate the current time corresponding to the animation value _value = _simulation! .x(elapsedInSeconds).clamp(lowerBound, upperBound); // 3. Set the state and cancel the callback function if (_simulation! .isDone(elapsedInSeconds)) { _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.completed : AnimationStatus.dismissed; stop(canceled: false); } // 4. NotifyListeners of changes in value (); // 5. Notify listener that status has changed _checkStatusChanged(); }Copy the code

The logic of the _tick method is

  1. The time of the current animation is calculated based on the timestamp called back by the SchedulerBinding.
  2. Then calculate the corresponding value according to the time;
  3. If the animation has completed, set the state and cancel the callback function.
  4. Notifies listeners that the value has changed
  5. Notifies listeners of status changes

At this point, the animation implementation logic is clear.

Calculation of the intermediate value of animation

We can see from the _tick method that the intermediate value of the animation is calculated in terms of elapsed time. The AnimationController defaults to 0-1, assuming the animation is 2s. If the curve is linear, then the value of the AnimationController will change to 0.5 when Elapsed is 1. This is easy to understand.

Animation time Animated value
0 0
0.5 0.25
1.0 0.5
1.5 0.75
2 1
  • What happens after you set up the CurvedAnimation?
class CurvedAnimation extends Animation<double> { double get value { final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve; final double t = parent.value; if (activeCurve == null) return t; If (t = = 0.0 | | t = = 1.0) {return t; } return activeCurve.transform(t); }}Copy the code

After the CurvedAnimation is set, the value of the animation is converted to the new value by calling Curve’s Transform method.

Decelerate, for example:

Double transformInternal(double t) {t = 1.0-t; Return 1.0-t * t; }Copy the code
Animation time Animated value
0 0
0.5 0.4375
1.0 0.75
1.5 0.9375
2 1
  • So what happens when you set Tween?

Tween also calls the transform method to perform a transform, for example:

Tween (the begin: 100.0, end: 200.0). The animate (_animation);

Animation time Animated value
0 100
0.5 143.75
1.0 175
1.5 193.75
2 200

The above logic is the logic used by the AnimationController, CurvedAnimation, and Tween to determine the intermediate value of the animation.

Why doesn’t the AnimatedWidget need to be refreshed manually?

abstract class AnimatedWidget extends StatefulWidget { @override _AnimatedState createState() => _AnimatedState(); } class _AnimatedState extends State<AnimatedWidget> { @override void initState() { super.initState(); widget.listenable.addListener(_handleChange); } void _handleChange() { setState(() { }); }}Copy the code

In the code we see that the AnimatedWidget inherits from the StatefulWidget. _AnimatedState adds an animation listener _handleChange function to initState. The _handleChange function calls setState to refresh.

How does AnimatedBuilder avoid refactoring of child widgets?

class AnimatedBuilder extends AnimatedWidget { const AnimatedBuilder({ Key? key, required Listenable animation, required this.builder, this.child, }) : assert(animation ! = null), assert(builder ! = null), super(key: key, listenable: animation); final Widget? child; @override Widget build(BuildContext context) { return builder(context, child); }}Copy the code

We can see that the constructor passes in the same child as the Builder, thus reaching the logic of child reuse.

Are you impressed by this reuse approach? Providers have a similar design.

How does ImplicitlyAnimatedWidget automatically animate?

abstract class ImplicitlyAnimatedWidget extends StatefulWidget { @override ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState(); } abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> { @protected // 1 AnimationController get controller => _controller; late final AnimationController _controller = AnimationController( duration: widget.duration, debugLabel: kDebugMode ? widget.toStringShort() : null, vsync: this, ); // 2 Animation<double> get animation => _animation; late Animation<double> _animation = _createCurve(); @override void initState() { super.initState(); _controller.addStatusListener((AnimationStatus status) { switch (status) { case AnimationStatus.completed: if (widget.onEnd ! = null) widget.onEnd! (a); break; case AnimationStatus.dismissed: case AnimationStatus.forward: case AnimationStatus.reverse: } }); _constructTweens(); didUpdateTweens(); } // 2. CurvedAnimation _createCurve() { return CurvedAnimation(parent: _controller, curve: widget.curve); } @override void dispose() { _controller.dispose(); super.dispose(); } bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) { return targetValue ! = (tween.end ?? tween.begin); } void _updateTween(Tween<dynamic>? tween, dynamic targetValue) { if (tween == null) return; tween .. begin = tween.evaluate(_animation) .. end = targetValue; } void didUpdateWidget(T oldWidget) { super.didUpdateWidget(oldWidget); if (widget.curve ! = oldWidget.curve) _animation = _createCurve(); _controller.duration = widget.duration; if (_constructTweens()) { forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) { _updateTween(tween, targetValue); return tween; }); _controller .. Value = 0.0.. forward(); didUpdateTweens(); } } bool _constructTweens() { bool shouldStartAnimation = false; forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) { if (targetValue ! = null) { tween ?? = constructor(targetValue); if (_shouldAnimateTween(tween, targetValue)) shouldStartAnimation = true; } else { tween = null; } return tween; }); return shouldStartAnimation; } @protected void forEachTween(TweenVisitor<dynamic> visitor); @protected void didUpdateTweens() { } }Copy the code

The code for the ImplicitlyAnimatedWidget is also clean and uses a combination of AnimationController, CurvedAnimation, and Tween, implemented internally.

When the property changes, the didUpdateWidget is called and the AnimationController’s Forward method is called to start the animation.

conclusion

This article through the analysis of the source code, the interpretation of some animation related content. Later we will enter into the use and analysis of state management, welcome to like and follow.