Principle of Flutter animation

In our last article we introduced the basic use of flutter animations, but how does the AnimationController receive the system’s frame draw callback?

We found that when using animation, must be with a TickerProvider implementation, check the TickProvider source code


TickerProvider

/// An interface implemented by classes that can vend [Ticker] objects.
///
/// Tickers can be used by any object that wants to be notified whenever a frame
/// triggers, but are most commonly used indirectly via an
/// [AnimationController]. [AnimationController]s need a [TickerProvider] to
/// obtain their [Ticker]. If you are creating an [AnimationController] from a
/// [State], then you can use the [TickerProviderStateMixin] and
/// [SingleTickerProviderStateMixin] classes to obtain a suitable
/// [TickerProvider]. The widget test framework [WidgetTester] object can be
/// used as a ticker provider in the context of tests. In other contexts, you
/// will have to either pass a [TickerProvider] from a higher level (e.g.
/// indirectly from a [State] that mixes in [TickerProviderStateMixin]), or
/// create a custom [TickerProvider] subclass.
abstract class TickerProvider {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const TickerProvider();

  /// Creates a ticker with the given callback.
  ///
  /// The kind of ticker provided depends on the kind of ticker provider.
  Ticker createTicker(TickerCallback onTick);
}
Copy the code
  • An interface implemented by a class that provides Ticker objects.
  • A Ticker can be used anywhere you want to receive a draw callback, but the most common isAnimationController
  • If you are creating an [AnimationController] from [State], You can use the [TickerProviderStateMixin] and [SingleTickerProviderStateMixin] to get a proper [TickerProvider]

TickerProvider creates a Ticker object. What is a Ticker object


Ticker

/// Calls its callback once per animation frame.
///
/// When created, a ticker is initially disabled. Call [start] to
/// enable the ticker.
///
/// A [Ticker] can be silenced by setting [muted] to true. While silenced, time
/// still elapses, and [start] and [stop] can still be called, but no callbacks
/// are called.
///
/// By convention, the [start] and [stop] methods are used by the ticker's
/// consumer, and the [muted] property is controlled by the [TickerProvider]
/// that created the ticker.
///
/// Tickers are driven by the [SchedulerBinding]. See
/// [SchedulerBinding.scheduleFrameCallback].
Copy the code

Each animation frame is called back once.

  • When created, the ticker’s initial state is disabled. Call [start] to start.
  • A [Ticker] can be muted by setting [conservation] to true. In the silent state, time still passes, and [start] and [stop] can still be called, but not called back into the Ticker.
  • The [start] and [stop] methods are used by the user of [ticker], and the [conservation] attribute is controlled by the [TickerProvider].
  • Ticker callbacks are driven by SchedulerBinding. [SchedulerBinding. ScheduleFrameCallback].

** Before each frame is drawn, a callback is made to the Ticker, driven by SchedulerBinding


ScheduleBinding

/// Scheduler for running the following:
///
/// * _Transient callbacks_, triggered by the system's [Window.onBeginFrame]
///   callback, for synchronizing the application's behavior to the system's
///   display. For example, [Ticker]s and [AnimationController]s trigger from
///   these.
///
/// * _Persistent callbacks_, triggered by the system's [Window.onDrawFrame]
///   callback, for updating the system's display after transient callbacks have
///   executed. For example, the rendering layer uses this to drive its
///   rendering pipeline.
///
/// * _Post-frame callbacks_, which are run after persistent callbacks, just
///   before returning from the [Window.onDrawFrame] callback.
///
/// * Non-rendering tasks, to be run between frames. These are given a
///   priority and are executed in priority order according to a
///   [schedulingStrategy].
Copy the code

Schedule these callbacks at runtime

  • Transient callbacks, called by the system’s [window.onbeginFrame] callback, used to synchronize the application’s behavior to the system display. For example, [Ticker]s and [AnimationController]s triggers come from it.
  • Persistent callbacksThe callback triggered by the system’s [window. onDrawFrame] method is used to update the system display after the TransientCallback execution. For example, the render layer uses it to drive the render pipeline for build, layout, and paint
  • _post-frame Callbacks_ Callback before drawing the next frame to do some cleaning and preparation
  • Non-rendering tasks, between frame constructs, are prioritized and are executed based on the priority of the schedulingStrategy, such as user input

**FrameCallback: The SchedulerBinding class has three FrameCallback callback queues, which are executed at different times during a drawing:

  1. TransientCallbacks: Used to store temporary callbacks, usually animation callbacks. Can be achieved bySchedulerBinding.instance.scheduleFrameCallbackAdd a callback.
  2. PersistentCallbacks: used to hold persistentCallbacks in which new frames cannot be drawn. PersistentCallbacks cannot be removed once registered.SchedulerBinding.instance.addPersitentFrameCallback()This callback handles layout and drawing.
  3. PostFrameCallbacks: will only be called once at the end of a Frame and will be removed by the systemSchedulerBinding.instance.addPostFrameCallback()Register, and be careful not to trigger a new Frame in such a callback, as this can cause a loop to refresh.
/// Schedules the given transient frame callback.
///
/// Adds the given callback to the list of frame callbacks and ensures that a
/// frame is scheduled.
///
/// If this is a one-off registration, ignore the `rescheduling` argument.
///
/// If this is a callback that will be re-registered each time it fires, then
/// when you re-register the callback, set the `rescheduling` argument to
/// true. This has no effect in release builds, but in debug builds, it
/// ensures that the stack trace that is stored for this callback is the
/// original stack trace for when the callback was _first_ registered, rather
/// than the stack trace for when the callback is re-registered. This makes it
/// easier to track down the original reason that a particular callback was
/// called. If `rescheduling` is true, the call must be in the context of a
/// frame callback.
///
/// Callbacks registered with this method can be canceled using
/// [cancelFrameCallbackWithId].
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
  scheduleFrame();
  _nextFrameCallbackId += 1;
  _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
  return _nextFrameCallbackId;
}

Copy the code

Schedule transient Frame callback queue

Add a callBack to ensure that fram can be called back to before it is drawn

void scheduleFrame() {
  if(_hasScheduledFrame || ! _framesEnabled)return;
  assert(() {
    if (debugPrintScheduleFrameStacks)
      debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase. ');
    return true; } ()); ensureFrameCallbacksRegistered();window.scheduleFrame();
  _hasScheduledFrame = true;
}
Copy the code
@protected
void ensureFrameCallbacksRegistered() {
  window.onBeginFrame ?? = _handleBeginFrame;window.onDrawFrame ?? = _handleDrawFrame; }Copy the code

The window.onbeginfram method is assigned

/// Called by the engine to prepare the framework to produce a new frame.
///
/// This function calls all the transient frame callbacks registered by
/// [scheduleFrameCallback]. It then returns, any scheduled microtasks are run
/// (e.g. handlers for any [Future]s resolved by transient frame callbacks),
/// and [handleDrawFrame] is called to continue the frame.
///
/// If the given time stamp is null, the time stamp from the last frame is
/// reused.
///
/// To have a banner shown at the start of every frame in debug mode, set
/// [debugPrintBeginFrameBanner] to true. The banner will be printed to the
/// console using [debugPrint] and will contain the frame number (which
/// increments by one for each frame), and the time stamp of the frame. If the
/// given time stamp was null, then the string "warm-up frame" is shown
/// instead of the time stamp. This allows frames eagerly pushed by the
/// framework to be distinguished from those requested by the engine in
/// response to the "Vsync" signal from the operating system.
///
/// You can also show a banner at the end of every frame by setting
/// [debugPrintEndFrameBanner] to true. This allows you to distinguish log
/// statements printed during a frame from those printed between frames (e.g.
/// in response to events or timers).
void handleBeginFrame(Duration rawTimeStamp) {
  Timeline.startSync('Frame', arguments: timelineWhitelistArguments); _firstRawTimeStampInEpoch ?? = rawTimeStamp; _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);if(rawTimeStamp ! =null)
    _lastRawTimeStamp = rawTimeStamp;
  _hasScheduledFrame = false;
  try {
    // TRANSIENT FRAME CALLBACKS
    Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
    _schedulerPhase = SchedulerPhase.transientCallbacks;
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
  	/ / * * * * * * * * * * * * to invoke the callBack * * * * * * * * * * * * * * * * * *
    _transientCallbacks = <int, _FrameCallbackEntry>{};
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
      if(! _removedIds.contains(id)) _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);//**************************callback(timeStamp);
    });
    _removedIds.clear();
  } finally{ _schedulerPhase = SchedulerPhase.midFrameMicrotasks; }}Copy the code
  • Called by engine when the frame is ready to build a frame, you can guess that endgine actually calls the Window’s onBegainFrame
  • It calls back all methods in [scheduleFrameCallback] (including some future methods), and when it’s done, handleDrawFrame is called.
/// Signature for frame-related callbacks from the scheduler.
///
/// The `timeStamp` is the number of milliseconds since the beginning of the
/// scheduler's epoch. Use timeStamp to determine how far to advance animation
/// timelines so that all the animations in the system are synchronized to a
/// common time base.
typedef FrameCallback = void Function(Duration timeStamp);
 callback(timeStamp);
Copy the code
  • The “timestamp” is the number of milliseconds since the Scheduler started. Use timestamps to determine how far the animation timeline should advance so that all animations in the system are synchronized to a common timeline.

Each time the system draws, a callback is called to the UI. Windows onBegainFrame, which executes the handleBeginFrame, calls back the time value to each callback. Note that this is before drawing, because we usually animate the control by changing its property value before drawing, and this action must be done before drawing.


Ticker

Reverse search for who called scheduleFrameCallback and find that it is scheduleTick in Ticker, and scheduleTick has several places to call later

/// Schedules a tick for the next frame.
///
/// This should only be called if [shouldScheduleTick] is true.
@protected
void scheduleTick({ bool rescheduling = false{})assert(! scheduled);assert(shouldScheduleTick);
  _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
Copy the code

Here we find that the ticker passed a _tick object to the scheduleFrameCallback

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
final TickerCallback _onTick;
Copy the code
Ticker(this._onTick, { this.debugLabel }) {
  assert(() {
    _debugCreationStack = StackTrace.current;
    return true; } ()); }Copy the code

So what you see here is that the _ticke method converts the timestamp to a relative time and calls back to onTick. OnTicker is created when the Ticker is constructed. However, Ticker creation is mainly in TickProvider

@override
Ticker createTicker(TickerCallback onTick) {
  _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
  return _ticker;
}
Copy the code

So who called this createTicker? It’s an 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,
}) : assert(lowerBound ! =null),
     assert(upperBound ! =null),
     assert(upperBound >= lowerBound),
     assert(vsync ! =null),
     _direction = _AnimationDirection.forward {
  _ticker = vsync.createTicker(_tick);
  _internalSetValue(value ?? lowerBound);
}
Copy the code
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);
  }
 ///noticed` notifyListeners()`The AnimationController inherits from the Animation from Listenable.
  notifyListeners();
  _checkStatusChanged();
}
Copy the code

Conclusion: * * ** After scheduleTick is called, the Ticker callback _tick(Duration Elapsed) is added to the transientCallbacks. And then we call this method through handleBeginFrame before each frame is drawn.


The one who calls scheduleTick calls this function by searching the Ticker source code for a start() place.

We usually use animation.forward() every time we start an animation,

TickerFuture forward({ double from }) {
  _direction = _AnimationDirection.forward;
  // If it is not null, the animation has a starting value
  if(from ! =null)
    value = from;
  return _animateToInternal(upperBound);
}
Copy the code

This method marks the animation as the start state, and if the animation is set to start at the start point, the default is the start point, then _animateToInternal is called

TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) {
  / / * * * * * * * * * * * * * * * * / /
  return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
}
Copy the code
TickerFuture _startSimulation(Simulation simulation) {
  _simulation = simulation;
  _lastElapsedDuration = Duration.zero;
  _value = simulation.x(0.0).clamp(lowerBound, upperBound);
 // Call ticker start here
  final TickerFuture result = _ticker.start();
  _status = (_direction == _AnimationDirection.forward) ?
    AnimationStatus.forward :
    AnimationStatus.reverse;
  _checkStatusChanged();
  return result;
}
Copy the code

** When the animationController’s forward is called, the ticker’s start method is finally called. The start method calls scheduleTick to start the ticker’s binding with ScheduleBinding. The entire process is a Windows- > ScheduleBinding -> Ticker ->AnimationController.


After the binding is established, the AnimationController’s _tick method is called back before each frame is drawn

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);
  }
  notifyListeners();
  _checkStatusChanged();
}
Copy the code

X (elapsedInSeconds).clamp(lowerBound, upperBound) calculates the value and notifyListeners() call back to see how it works. Here is the exact call sequence