Flutter 如何判斷 Widget 位於前臺

壞de牧羊人發表於2021-05-14

目錄

  • 研究背景
  • 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);
    }
  }
}

複製程式碼

TickerSchedulerBinding 驅動。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 就是我們在 Statevsync:this ,它做了一個橋樑連線了 StateTicker

以上原始碼重要一點:是在 didChangeDependencies() 中將 muted = !TickerMode.of(context) 初始化一遍。 xxx.of(context) 一看就是 InheritedWidgetwidget 中的屬性。

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

根據 OverlayEntryopaque 屬性,判斷哪些 OverlayEntry 在前臺(onstage)的tickerEnabledtrue 後臺為 false

Navigator 負責將頁面棧中所有頁面包含的 OverlayEntry 組織成一個 List,傳遞給 Overlay,也就是說每一個頁面都有一個OverlayEntry

所以解釋了前臺頁面 AnimationController 會呼叫其回撥並播放動畫,後臺頁面AnimationController即使時間在流逝並不會播放動畫。

解決問題

解決問題很簡單在 Swiperautoplay 引數中加入 TickerMode.of(context)這樣切換到下一個頁面Swiper就不會自動播放了。

Swiper(
      ...
      autoplay: autoplay && TickerMode.of(context),
      ...
    );
複製程式碼

至於 Swiper 為什麼切換到下一個頁面仍自動播放,有興趣可以看 banner 原始碼實現,這裡不過多講述。

總結

根據以上,能得出如下結論:

  1. 當傳入 vsync:this 相當於告訴 AnimationController 當前 Widget 是否處於前臺Widget處於後臺的時,動畫時間會流失但不會呼叫其回撥。flutter這樣做的目的是減少不必要的效能消耗。
  2. TickerMode.of(context) == true 表明當前 Widget 處於前臺頁面,反之則說明在後臺

相關文章