目錄
- 研究背景
- AnimationController
- Ticker
- SingleTickerProviderStateMixin
- Overlay
- 解決問題
- 總結
研究背景:
在專案中我們的 banner第三方控制元件實現的。
當頁面切換到後臺時 banner
仍自動播放,但我們用 AnimationController
實現的動畫卻停止了,於是我開始尋找原因。
在 flutter
中使用動畫,就會用到 AnimationController
物件,通常我們是這樣構造它:
class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, // the SingleTickerProviderStateMixin
duration: widget.duration,
);
}
複製程式碼
那麼傳入 vsync:this
之後做了什麼,還有為什麼要傳入它?
AnimationController
通常使用用 AnimationController.value
的值配合 setState()
來做動畫。
AnimationController
建構函式如下:
AnimationController({
this.duration,
...
required TickerProvider vsync,
}) : _direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
}
void _tick(Duration elapsed) {
...
_value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
...
notifyListeners();
_checkStatusChanged();
}
複製程式碼
通過 vsync
物件建立了一個 _ticker
,而傳入的 _tick
是一個回撥函式。檢視原始碼它是用於更新
value
,也就是說 AnimationController.value
是在此回撥中發生改變。
我們將視角回撥 _ticker = vsync.createTicker(_tick);
來看看 Ticker
。
Ticker
以下原始碼有刪減,不想看可直接往下拉
class Ticker {
TickerFuture? _future;
bool get muted => _muted;
bool _muted = false;
set muted(bool value) {
if (value == muted)
return;
_muted = value;
if (value) {
unscheduleTick();
} else if (shouldScheduleTick) {
scheduleTick();
}
}
bool get isTicking {
if (_future == null)
return false;
if (muted)
return false;
if (SchedulerBinding.instance!.framesEnabled)
return true;
if (SchedulerBinding.instance!.schedulerPhase != SchedulerPhase.idle)
return true; // for example, we might be in a warm-up frame or forced frame
return false;
}
@protected
bool get shouldScheduleTick => !muted && isActive && !scheduled;
void _tick(Duration timeStamp) {
assert(isTicking);
assert(scheduled);
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime!);
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
@protected
void scheduleTick({ bool rescheduling = false }) {
assert(!scheduled);
assert(shouldScheduleTick);
_animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
@protected
void unscheduleTick() {
if (scheduled) {
SchedulerBinding.instance!.cancelFrameCallbackWithId(_animationId!);
_animationId = null;
}
assert(!shouldScheduleTick);
}
@mustCallSuper
void dispose() {
if (_future != null) {
final TickerFuture localFuture = _future!;
_future = null;
assert(!isActive);
unscheduleTick();
localFuture._cancel(this);
}
}
}
複製程式碼
Ticker
由 SchedulerBinding
驅動。flutter
每繪製一幀就會回撥 Ticker._onTick()
,所以每繪製一幀 AnimationController.value
就會發生變化。
接下來看一下 Ticker
其他成員與方法:
muted
: 設定為ture
時鐘仍然可以執行,但不會呼叫該回撥。isTicking
: 是否可以在下一幀呼叫其回撥,如裝置的螢幕已關閉,則返回false。_tick()
: 時間相關的計算交給 _onTick(),受到muted
影響。scheduleTick()
: 將_tick()
回撥交給SchedulerBinding
管理,flutter
每繪製一幀都會呼叫它。unscheduleTick()
: 取消回撥的監聽。
SingleTickerProviderStateMixin
@optionalTypeArgs
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
Ticker? _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null)
return _ticker!;
}
@override
void dispose() {
super.dispose();
}
@override
void didChangeDependencies() {
if (_ticker != null)
_ticker!.muted = !TickerMode.of(context);
super.didChangeDependencies();
}
}
複製程式碼
SingleTickerProviderStateMixin
就是我們在 State
中 vsync:this
,它做了一個橋樑連線了 State
與 Ticker
。
以上原始碼重要一點:是在 didChangeDependencies()
中將 muted = !TickerMode.of(context)
初始化一遍。 xxx.of(context)
一看就是 InheritedWidget
中 widget
中的屬性。
Overlay
最終找到了 Overlay
@override
Widget build(BuildContext context) {
final List<Widget> children = <Widget>[];
bool onstage = true;
int onstageCount = 0;
for (int i = _entries.length - 1; i >= 0; i -= 1) {
final OverlayEntry entry = _entries[i];
if (onstage) {
onstageCount += 1;
children.add(_OverlayEntryWidget(
key: entry._key,
entry: entry,
));
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
children.add(_OverlayEntryWidget(
key: entry._key,
entry: entry,
tickerEnabled: false,
));
}
}
return _Theatre(
skipCount: children.length - onstageCount,
children: children.reversed.toList(growable: false),
clipBehavior: widget.clipBehavior,
);
}
複製程式碼
根據 OverlayEntry
的 opaque
屬性,判斷哪些 OverlayEntry
在前臺(onstage)的tickerEnabled
為 true
後臺為 false
。
Navigator
負責將頁面棧中所有頁面包含的 OverlayEntry
組織成一個 List
,傳遞給 Overlay
,也就是說每一個頁面都有一個OverlayEntry
所以解釋了前臺頁面 AnimationController
會呼叫其回撥並播放動畫,後臺頁面AnimationController
即使時間在流逝並不會播放動畫。
解決問題
解決問題很簡單在 Swiper
的 autoplay
引數中加入 TickerMode.of(context)
這樣切換到下一個頁面Swiper
就不會自動播放了。
Swiper(
...
autoplay: autoplay && TickerMode.of(context),
...
);
複製程式碼
至於 Swiper
為什麼切換到下一個頁面仍自動播放,有興趣可以看 banner 原始碼實現,這裡不過多講述。
總結
根據以上,能得出如下結論:
- 當傳入
vsync:this
相當於告訴AnimationController
當前Widget
是否處於前臺。Widget
處於後臺的時,動畫時間會流失但不會呼叫其回撥。flutter
這樣做的目的是減少不必要的效能消耗。TickerMode.of(context) == true
表明當前Widget
處於前臺頁面,反之則說明在後臺。