Flutter visibility_detector 原理解析

4,089 阅读4分钟

简介

visibility_detector 是由谷歌官方团队开发的一款拓展组件,主要用于监听子组件可见性的变化。

背景

日常开发场景中,经常遇到一些需求需要在 Widget 可见性发生变化的时候进行操作,比如视频播放组件,需要让其在切换页面后自动停止播放,又比如 Banner 轮播图组件,需要在 Banner 过页时,让其停止轮播以达到优化性能的效果。

那么应该怎么去实现?

首先会想到的是用State的生命周期,initState 初始化,dispose 关闭,但这有一个问题,push一个新页面是不会触发 dispose 的,所以这是行不通的。

那么用 WidgetsBindingObserver 呢?

WidgetsBinding.instance.addObserver(this);
...
  
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if(state == AppLifecycleState.paused){
    ...
  } else if(state == AppLifecycleState.resumed){
    ...
  }
}

如果亲自尝试会发现这也是行不通的,因为 didChangeAppLifecycleState 只会监听整个app的变化,而不是监听当前widget,在push一个新页面时也不会触发回调。

最后发现实现方案只能用 NavigatorObserver 去监听路由栈的变化,在 didPush 和 didPop 中获取路由信息并手动维护一个全局的 Manager,Widget 在使用时向 Manager 去注册路由变化的通知回调同时配合 WidgetsBindingObserver 就可以了。但这未免也太麻烦了,难道没有更简单的方法吗?

直到最后我发现了 visibility_detector ,使用方法如下,是不是很简单?

@override
Widget build(BuildContext context) {
  return VisibilityDetector(
    key: Key('my-widget-key'),
    onVisibilityChanged: (visibilityInfo) {
      var visiblePercentage = visibilityInfo.visibleFraction * 100;
      debugPrint(
          'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible');
    },
    child: someOtherWidget,
  );
}

原理解析

VisibilityDetector 使用的不是如上所说的 NavigatorObserver 方法,而是另辟蹊径,采用了更巧妙的方法。

那是如何监听显示状态发生变化的呢?看看源码便知,VisibilityDetector 的源码很简单,基本实现功能只用了三个类。

class VisibilityDetector extends SingleChildRenderObjectWidget {

  const VisibilityDetector({
    @required Key key,
    @required Widget child,
    @required this.onVisibilityChanged,
  })  : assert(key != null),
        assert(child != null),
        super(key: key, child: child);

  final VisibilityChangedCallback onVisibilityChanged;

  @override
  RenderVisibilityDetector createRenderObject(BuildContext context) {
    return RenderVisibilityDetector(
      key: key,
      onVisibilityChanged: onVisibilityChanged,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, RenderVisibilityDetector renderObject) {
    assert(renderObject.key == key);
    renderObject.onVisibilityChanged = onVisibilityChanged;
  }
}

VisibilityDetector 继承自 SingleChildRenderObjectWidget 类,而 SingleChildRenderObjectWidget 想必大家都见过,很多组件如 Align、Padding 等都继承自 SingleChildRenderObjectWidget,用于生成需要渲染的RenderObject。

接下来看 RenderVisibilityDetector 类。

class RenderVisibilityDetector extends RenderProxyBox {
  
  RenderVisibilityDetector({
    RenderBox child,
    @required this.key,
    @required VisibilityChangedCallback onVisibilityChanged,
  })  : assert(key != null),
        _onVisibilityChanged = onVisibilityChanged,
        super(child);

  省略一些代码...
  
  @override
  void paint(PaintingContext context, Offset offset) {
    if (onVisibilityChanged == null) {
      // No need to create a [VisibilityDetectorLayer].  However, in case one
      // already exists, remove all cached data for it so that we won't fire
      // visibility callbacks when the layer is removed.
      VisibilityDetectorLayer.forget(key);
      super.paint(context, offset);
      return;
    }

    final layer = VisibilityDetectorLayer(
        key: key,
        widgetSize: semanticBounds.size,
        paintOffset: offset,
        onVisibilityChanged: onVisibilityChanged);
    context.pushLayer(layer, super.paint, offset);
  }
}

在 RenderVisibilityDetector 中,在绘制方法 paint() 里 push 了一个 Layer,这个Layer就是 VisibilityDetector 用于监听可见状态的关键。

那么 Layer 是什么?

Layer 表示的是图层的意思。通常一棵 RenderObject 树经过绘制之后,就会生成一个 Layer 对象,但并不是所有 RenderObject 都会绘制到一个 Layer 中,某些情况下,例如不同路由页面,就会绘制到不同的 Layer 图层中。这些 Layer 对象组成的结构就是 Layer 树。

aHR0cHM6Ly9naXRlZS5jb20vYXJjdGljZm94MTkxOS9JbWFnZUhvc3RpbmcvcmF3L21hc3Rlci9pbWcvU25pcGFzdGVfMjAyMC0wNy0xMF8xNy0yMC01My5qcGc.jpg

再看回 VisibilityDetectorLayer

class VisibilityDetectorLayer extends ContainerLayer {
  
  ...省略一些代码
    
  static Timer _timer;
  
    @override
  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
    _scheduleUpdate();
    super.addToScene(builder, layerOffset);
  }

  @override
  void attach(Object owner) {
    super.attach(owner);
    _scheduleUpdate();
  }

  @override
  void detach() {
    super.detach();
    _scheduleUpdate();
  }
  
  void _scheduleUpdate() {
    ...省略一些代码
     if(_timer == null){
       _timer = Timer(updateInterval, _handleTimer);
     }
  }
  
  void _handleTimer(){
    _timer == null;
    ...延迟500ms后,检测layer宽高
  }
}

在 Layer 附加到与移除屏幕时, addToScene(),attach(),detach()这三个方法会收到回调,所以这就是检测的时机。在收到回调后,会启动一个延时500ms的定时器,定时器有两个作用,一是确保子 Widget 已绘制完成,二是因为在屏幕在需要多次绘制时可能会添加多个 Layer,所以需要用定时器进行过滤(注意这里 _timer 是 static 类型)。

定时器执行的任务 _handleTimer() 主要是计算 Layer 的宽高,通过 Layer 宽高大小去间接判断子组件是否可见。(注:这里计算宽高的方法比较复杂,这里只作简要说明,深入理解需看源码)

总结

VisibilityDetector 通过在子Widget 上放置一个 Layer 来间接检测可见状态,这是一个很巧妙的做法。

但是也存在局限性,因为是通过宽高判断,所以是无法识别子 Widget 的透明度变化的。