Flutter 如何判断 Widget 位于前台

目录

  • 研究背景
  • 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 处于前台页面,反之则说明在后台
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享