Flutter AnimationController回调原理

2,761 阅读5分钟

Flutter动画原理

上一篇我们介绍了flutter动画的基本使用,但AnimationController是如何接受到系统frame绘制的回调?

我们发现在使用动画的时候,一定要with一个TickerProvider实现,查看TickProvider的源码可知


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);
}
  • 一个由类实现的接口,可以提供Ticker对象。
  • Ticker可以用在任何想要收到绘制回调的地方,但最常用的是AnimationController
  • 如果您正在从 [State]创建一个[AnimationController],那么您可以使用[TickerProviderStateMixin]和[SingleTickerProviderStateMixin]类来获取一个合适的 [TickerProvider]

**总结:**TickerProvider的作用创建一个Ticker对象,那Ticker对象是什么


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].

每个动画帧会回调一次。

  • 创建时,ticker的初始状态是禁用的。调用[start]启动。
  • A [Ticker]可以通过将[muted]设置为true来进行沉默。在沉默状态下,时间仍然在流逝,[start]和[stop]仍然可以被调用,但是不会回调到Ticker中。
  • 按照约定,[start]和[stop]方法由[ticker]的使用者使用,[muted]属性由[TickerProvider] 控制。
  • Ticker回调是由[SchedulerBinding]驱动的。[SchedulerBinding.scheduleFrameCallback]。

**总结: **每次frame绘制之前会回调到ticker,由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].

调度运行时的这些回调

  • Transient callbacks,由系统的[Window.onBeginFrame]回调,用于同步应用程序的行为到系统的展示。例如,[Ticker]s和[AnimationController]s触发器来自与它。
  • Persistent callbacks 由系统的[Window.onDrawFrame]方法触发回调,用于在TransientCallback执行后更新系统的展示。例如,渲染层使用他来驱动渲染管道进行build,layout,paint
  • _Post-frame callbacks_在下一帧绘制前回调,主要做一些清理和准备工作
  • Non-rendering tasks 非渲染的任务,在帧构造之间,他们具有优先级,通过[schedulingStrategy]的优先级进行执行,例如用户的输入

**总结:**FrameCallback:SchedulerBinding 类中有三个FrameCallback回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行:

  1. transientCallbacks:用于存放一些临时回调,一般存放动画回调。可以通过SchedulerBinding.instance.scheduleFrameCallback 添加回调。
  2. persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。
  3. postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由 SchedulerBinding.instance.addPostFrameCallback() 注册,注意,不要在此类回调中再触发新的Frame,这可以会导致循环刷新。
/// 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;
}

调度 transient frame callback队列

添加一个callBack,确保fram绘制之前能够回调到他

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

赋给了window.onBeginFram方法

/// 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;
  	//************调用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;
  }
}
  • 当框架准备构建一帧的时候由engine调用,可以猜想endgine调用的实际上是window的onBegainFrame
  • 它会回调[scheduleFrameCallback]中的所有方法(包括一些future的方法),当执行完成后,系统会调用[handleDrawFrame]
/// 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);
  • “时间戳”是自scheduler开始以来的毫秒数。使用时间戳来确定动画时间线要前进多远,以便系统中的所有动画都同步到一通用的时间线上。

**总结:**当每次系统绘制的之前,会回调到ui.windows的onBegainFrame,而这个onBegainFrame执行的是handleBeginFrame,将时间值回调给了每一个callback。这里注意的是,是在在绘制之前,因为我们一般在绘制之前去通过改变控件的属性值完成动画,而这个动作必须在绘制前完成。


Ticker

反向搜索谁调用了scheduleFrameCallback,发现是在Ticker中的scheduleTick,而scheduleTick有几个地方调用后面来看

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

这里发现ticker传入了一个_tick对象到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);
}
final TickerCallback _onTick;
Ticker(this._onTick, { this.debugLabel }) {
  assert(() {
    _debugCreationStack = StackTrace.current;
    return true;
  }());
}

这里发现,_ticke方法,将时间戳,转换为一个相对时间,回调的到onTick中。onTicker是Ticker构造时候创建的。而Ticker的创建其实主要只在TickProvider中

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

那谁调用了这个createTicker呢,其实自然能联想到是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);
}
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()`方法,果然AnimationController继承自Animation继承自Listenable。
  notifyListeners();
  _checkStatusChanged();
}

**总结:**某个方法调用了scheduleTick之后,使的Ticker里面的回调函数_tick(Duration elapsed)被添加到transientCallbacks中,之后每次帧绘制之前候通过handleBeginFrame回调到这个方法。


那谁调用了scheduleTick,通过源码搜索Ticker中的一个start()的地方调用了这个函数。

每次启动动画的时候我们一般使用animation.forward(),

TickerFuture forward({ double from }) {
  _direction = _AnimationDirection.forward;
  //如果不为null 表示动画有起始值
  if (from != null)
    value = from;
  return _animateToInternal(upperBound);
}

这个方法就两个作用,一个是将动画标记为开始状态,另一个作用如果为动画设置从哪一个位置开始,默认从起始点,之后调用_animateToInternal

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

**总结:**当调用animationController的forward的时候,最终会调到ticker的start方法,start方法调用scheduleTick开始了ticker与ScheduleBinding的绑定。整个过程是一个Windows- > ScheduleBinding -> Ticker ->AnimationController。


建立好绑定之后,每次帧绘制之前,都会回调到AnimationController的_tick方法中

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

通过_simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound)计算出value值,然后由 notifyListeners()回调给我们的具体使用。 下面是具体的调用时序