Introduction to the

Visibility_detector is an extension component developed by Google’s official team to listen for changes in the visibility of child components.

background

In daily development scenarios, we often encounter some requirements that need to be operated when the visibility of the Widget changes, such as the video playback component, which needs to stop playing automatically after switching pages, or the Banner rotation diagram component, which needs to stop playing when the Banner passes across the page to optimize performance.

So how do you do that?

The first thing that comes to mind is to use the life cycle of State, initState initializes and Dispose closes, but there’s a problem, pushing a new page won’t trigger Dispose, so it won’t work.

What about WidgetsBindingObserver?

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

If try will find it and it won’t work, because didChangeAppLifecycleState will only to monitor the change of the app, rather than to monitor the current widget, when push a new page will not trigger a callback.

Finally, it is found that the implementation can only use NavigatorObserver to monitor routing stack changes, obtain routing information in didPush and didPop, and manually maintain a global Manager. The Widget registers notification callbacks of route changes to the Manager when used in conjunction with the WidgetsBindingObserver. But that’s too much trouble. Isn’t there an easier way?

Finally, I found visibility_detector. The method of use is as follows, isn’t it very simple?

@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,
  );
}
Copy the code

The principle of analytic

Instead of using the NavigatorObserver method described above, VisibilityDetector takes a different and more ingenious approach.

So how do you listen for changes in the display state? Look at the source code, VisibilityDetector’s source code is very simple, the basic implementation function is only three classes.

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; }}Copy the code

VisibilityDetector inherited from SingleChildRenderObjectWidget class and SingleChildRenderObjectWidget we all seen, Many components such as the Align, Padding, etc are inherited from SingleChildRenderObjectWidget, used to generate the RenderObject need rendering.

Next look at the RenderVisibilityDetector class.

class RenderVisibilityDetector extends RenderProxyBox {
  
  RenderVisibilityDetector({
    RenderBox child,
    @required this.key,
    @required VisibilityChangedCallback onVisibilityChanged,
  })  : assert(key ! =null),
        _onVisibilityChanged = onVisibilityChanged,
        super(child); Leave out some code...@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); }}Copy the code

In RenderVisibilityDetector, a Layer is pushed in the paint() method. This Layer is the key VisibilityDetector uses to listen for visible state.

So what is Layer?

Layer means Layer. Usually, a RenderObject tree is drawn to generate a Layer object, but not all renderObjects are drawn to one Layer. In some cases, such as different routing pages, renderObjects are drawn to different Layer layers. The structure of these Layer objects is the Layer tree.

Then look at VisibilityDetectorLayer back

class VisibilityDetectorLayer extends ContainerLayer {... Leave out some codestatic 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() { ... Leave out some codeif(_timer == null){ _timer = Timer(updateInterval, _handleTimer); }}void _handleTimer(){
    _timer == null; . delay500After ms, check layer width and height}}Copy the code

The addToScene(), Attach (), and detach() methods receive callbacks when Layer attaches to and removes the screen, so this is the time to check. After the callback is received, a timer with a delay of 500ms is started. The timer is used to ensure that the child Widget is drawn and to filter the screen because multiple layers may be added when the screen is drawn multiple times (note that _timer is static).

The task _handleTimer() performed by timer mainly calculates the width and height of the Layer, and indirectly determines whether the sub-components are visible by the width and height of the Layer. (Note: the calculation of the width and height of the method is more complex, here is only a brief explanation, in-depth understanding to see the source code)

conclusion

The VisibilityDetector indirectly detects visible state by placing a Layer on the child Widget, which is a neat trick.

However, there are limitations, because the transparency of the child widgets cannot be identified by width and height.