Flutter動畫原始碼解析

chonglingliu發表於2021-04-09

上篇文章我們詳細介紹了動畫的使用,本文我們將從原始碼的角度解析動畫的底層邏輯。

動畫的實現機制

動畫的控制是由AnimationController來實現的,這樣我們就先從AnimationController來入手研究。

  • AnimationController建構函式
<!-- 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) {
    // 省略內容...
}

Ticker? _ticker;
複製程式碼

AnimationController的建構函式中用vsync.createTicker(_tick)方法初始化了屬性_ticker,並設定了動畫的初始值。

_ticker中一個重要的作用是持有了一個回撥函式void _tick(Duration elapsed),這個_tick回撥函式的作用我們後面會詳細解釋。

  • AnimationControllerforward方法
<!-- 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();
    // 省略內容...
    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);
}
複製程式碼

forward方法中先呼叫了_ticker!.stop(canceled: true),然後呼叫了_ticker!.start()方法。

上面提到的兩個方法都是_ticker的方法,那它們做了什麼工作呢?

  • Ticker
<!-- Ticker -->
void stop({ bool canceled = false }) {
    // 取消排程Tick
    unscheduleTick();
}
  
TickerFuture start() {
    // 省略內容...
    if (shouldScheduleTick) {
      // 1 開始排程Tick
      scheduleTick();
    }
    if (SchedulerBinding.instance!.schedulerPhase.index > SchedulerPhase.idle.index &&
        SchedulerBinding.instance!.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
      // 2 記錄動畫開始時間
      _startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
    return _future!;
}  
複製程式碼
  1. stop方法呼叫了unscheduleTick方法取消Tick排程
  2. start方法呼叫了scheduleTick方法開始Tick排程,並且記錄了開始時間。

Tick排程 是什麼意思呢?

<!-- Ticker -->
void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}

void unscheduleTick() {
    if (scheduled) {
      SchedulerBinding.instance!.cancelFrameCallbackWithId(_animationId!);
      _animationId = null;
    }
}
複製程式碼
<!-- 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);
}
複製程式碼
  1. scheduleTick是將AnimationController_tick加入到了SchedulerBinding_transientCallbacks陣列中,然後返回了一個對應的回撥ID,然後請求重新整理介面。
  2. unscheduleTick是根據回撥IDAnimationController_tickSchedulerBinding_transientCallbacks陣列中移除。
  • SchedulerBinding

進入了我們熟悉的SchedulerBinding了,如果你有閱讀過前面的文章應該對它的功能和呼叫邏輯有印象。

void handleBeginFrame(Duration? rawTimeStamp) {
    // 1. 時間
    _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _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 (!_removedIds.contains(id))
          // 2 回撥
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
      });
      _removedIds.clear();
    } finally {
    }
}
複製程式碼

每次進行介面重新整理的時候,Flutter Engine會回撥SchedulerBindinghandleBeginFrame方法,還傳過來了一個時間戳。這時候會一一呼叫_transientCallbacks陣列中的回撥函式,並且將時間戳傳進去。

handleBeginFrame是在重新整理介面函式_handleDrawFrame之前呼叫的,這裡我們就可以知道handleBeginFrame主要是為了在繪製之前處理和設定好動畫的中間值,便於重新繪製。

我們回過來再看看AnimationController_tick的邏輯。

  • _tick方法
void _tick(Duration elapsed) {
    
    _lastElapsedDuration = elapsed;
    // 1. 計算已經動畫的時間
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    // 2. 計算當前時間對應的動畫的值
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    // 3. 如果動畫已經完成進行狀態的設定和回撥函式的取消
    if (_simulation!.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    // 4. 通知監聽者value發生了變化
    notifyListeners();
    // 5. 通知監聽者status發生了變化
    _checkStatusChanged();
}
複製程式碼

_tick方法的邏輯是

  1. 先根據SchedulerBinding回撥過來的時間戳算出當前動畫的時間;
  2. 然後根據時間算出對應的value值;
  3. 如果動畫已經完成進行狀態的設定和回撥函式的取消;
  4. 通知監聽者value發生了變化
  5. 通知監聽者status發生了變化

至此,動畫的實現邏輯就清晰了。

動畫邏輯

動畫的中間值的計算

我們從_tick方法中看到了動畫的中間值是根據時間elapsed來計算的。AnimationController預設是從0-1,假設動畫的時長是2s。如果曲線是速率變化線性的,那麼elapsed是1的時候,AnimationControllervalue就變成了0.5。這個很好理解。

動畫時間動畫值
00
0.50.25
1.00.5
1.50.75
21
  • 設定了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);
    }
}
複製程式碼

設定了CurvedAnimation後,動畫的值會呼叫Curvetransform方法,轉換成新的值。

Curve.decelerate為例:

double transformInternal(double t) {
    t = 1.0 - t;
    return 1.0 - t * t;
}
複製程式碼
動畫時間動畫值
00
0.50.4375
1.00.75
1.50.9375
21
  • 設定了Tween之後呢?

Tween其實也是呼叫了transform方法進行了一次轉換,例如:

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

動畫時間動畫值
0100
0.5143.75
1.0175
1.5193.75
2200

上面的邏輯就是AnimationControllerCurvedAnimationTween一起共同決定動畫中間值的一個邏輯。

AnimatedWidget為什麼不需要手動重新整理?

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

}
複製程式碼

程式碼中我們看到AnimatedWidget繼承自StatefulWidget_AnimatedStateinitState加入了一個動畫的監聽_handleChange函式。_handleChange函式中呼叫了setState進行重新整理。

AnimatedBuilder如何避免了子Widget的重構?

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);
  }
}
複製程式碼

程式碼中我們看到建構函式傳入的childbuilder方法傳出去的child是同一個,這樣就達到了child的複用邏輯。

是否對這種複用方式有印象?Provider其實也有類似的設計。

AnimatedBuilder

ImplicitlyAnimatedWidget如何實現自動動畫的?

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!();
          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() { }
}
複製程式碼

ImplicitlyAnimatedWidget的程式碼也很清晰,使用的也是AnimationControllerCurvedAnimationTween的組合,只是內部實現了。

當屬性發生變化的時候,會呼叫didUpdateWidget,然後呼叫AnimationControllerforward方法開始動畫。

總結

本文通過原始碼的分析,解讀了動畫的一些相關內容。後面我們將會進入狀態管理的使用和分析,歡迎點贊和關注。

相關文章