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 处于前台页面,反之则说明在后台
相关文章
相关标签/搜索