Flutter 如何判斷 Widget 位於前臺

目錄

  • 研究背景
  • AnimationController
  • Ticker
  • SingleTickerProviderStateMixin
  • Overlay
  • 解決問題
  • 總結

研究背景:

在項目中咱們的 banner第三方控件實現的。markdown

當頁面切換到後臺時 banner 仍自動播放,但咱們用 AnimationController 實現的動畫卻中止了,因而我開始尋找緣由。app

flutter 中使用動畫,就會用到 AnimationController 對象,一般咱們是這樣構造它:ide

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() 來作動畫。oop

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); 來看看 Tickerui

Ticker

如下源碼有刪減,不想看可直接往下拉this

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 就會發生變化。lua

接下來看一下 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 處於前臺頁面,反之則說明在後臺
相關文章
相關標籤/搜索