上篇文章我們詳細介紹了動畫的使用,本文我們將從原始碼的角度解析動畫的底層邏輯。
動畫的實現機制
動畫的控制是由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
回撥函式的作用我們後面會詳細解釋。
- AnimationController的
forward
方法
<!-- 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!;
}
複製程式碼
stop
方法呼叫了unscheduleTick
方法取消Tick排程;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);
}
複製程式碼
scheduleTick
是將AnimationController的_tick
加入到了SchedulerBinding的_transientCallbacks
陣列中,然後返回了一個對應的回撥ID,然後請求重新整理介面。unscheduleTick
是根據回撥ID將AnimationController的_tick
從SchedulerBinding的_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會回撥SchedulerBinding的
handleBeginFrame
方法,還傳過來了一個時間戳。這時候會一一呼叫_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
方法的邏輯是
- 先根據SchedulerBinding回撥過來的時間戳算出當前動畫的時間;
- 然後根據時間算出對應的value值;
- 如果動畫已經完成進行狀態的設定和回撥函式的取消;
- 通知監聽者value發生了變化
- 通知監聽者status發生了變化
至此,動畫的實現邏輯就清晰了。
動畫的中間值的計算
我們從_tick
方法中看到了動畫的中間值是根據時間elapsed
來計算的。AnimationController預設是從0-1,假設動畫的時長是2s。如果曲線是速率變化線性的,那麼elapsed
是1的時候,AnimationController的value
就變成了0.5。這個很好理解。
動畫時間 | 動畫值 |
---|---|
0 | 0 |
0.5 | 0.25 |
1.0 | 0.5 |
1.5 | 0.75 |
2 | 1 |
- 設定了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後,動畫的值會呼叫Curve的transform
方法,轉換成新的值。
以Curve.decelerate為例:
double transformInternal(double t) {
t = 1.0 - t;
return 1.0 - t * t;
}
複製程式碼
動畫時間 | 動畫值 |
---|---|
0 | 0 |
0.5 | 0.4375 |
1.0 | 0.75 |
1.5 | 0.9375 |
2 | 1 |
- 設定了Tween之後呢?
Tween其實也是呼叫了transform
方法進行了一次轉換,例如:
Tween(begin: 100.0, end: 200.0).animate(_animation);
動畫時間 | 動畫值 |
---|---|
0 | 100 |
0.5 | 143.75 |
1.0 | 175 |
1.5 | 193.75 |
2 | 200 |
上面的邏輯就是AnimationController,CurvedAnimation和Tween一起共同決定動畫中間值的一個邏輯。
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,_AnimatedState
在initState
加入了一個動畫的監聽_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);
}
}
複製程式碼
程式碼中我們看到建構函式傳入的child和builder
方法傳出去的child是同一個,這樣就達到了child的複用邏輯。
是否對這種複用方式有印象?Provider其實也有類似的設計。
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的程式碼也很清晰,使用的也是AnimationController,CurvedAnimation和Tween的組合,只是內部實現了。
當屬性發生變化的時候,會呼叫didUpdateWidget
,然後呼叫AnimationController的forward
方法開始動畫。
總結
本文通過原始碼的分析,解讀了動畫的一些相關內容。後面我們將會進入狀態管理的使用和分析,歡迎點贊和關注。