First, existing exposure problems

Some user data buries and analyses are particularly important for tracking user behavior. Exposure is one of the common buries, such as blocks A, B, C, and D shown in the figure below. Common practices may occur when dom is mounted, such as the mount method of A Vue component. It is not accurate to judge whether the node is mounted or not as the exposure reporting time. If the product has more detailed requirements on exposure, for example

  • The pit needs to slide out of a certain proportion before starting exposure
  • The pit must remain in the visible area of the screen for a certain amount of time before the exposure event is triggered
  • The pit is out of view, and should be reported again when it slips into view again
  • The pit repeatedly moves up and down in the field of view triggering only one exposure

As shown in the figure above, for A, B, C, D

  • Pit A has slid out of the visible area of the screen and is inAn invisible stateThis is not called exposure;
  • Pit B is about to slide up from the visible area of the screen, fromvisibleinvisible;
  • Pit C is in the central visual area of the screen, yesVisible state, can trigger exposure;
  • Pit D is about to slide into the visible area of the screen,From invisible to visibleWhen the visible proportion reaches a certain threshold, exposure behavior can be triggered;

Therefore, strictly speaking, A and D on the page do not count as exposures, but D can trigger A report when it slides back into the visible area for A certain proportion of its dwell time. This situation is particularly common in lists. How to achieve A more accurate report in Flutter

Ii. Flutter reporting scheme

In Flutter, you can detect the display scale of modules on the page, as shown in the figure below. When the list is loaded, the active items in the list are

[0-0] [0, 1] [2-0]

1-1] [1-0] [[1-2]

2-1] [2-0] [[2-2]

[3-1] [3-2] [3-3]

4-1] [4-0] [(4-2)

[5-0] [5-1] [5-2]

Three, principle analysis

Refer to the previous two sections (Layout and Paint) for this process.

Build the Widget tree, create the corresponding Element with createElement, and create the corresponding renderObject Layout. Paint: Build a Layer that triggers the attach method when the Layer appends to the parent node

Therefore, the visibility of the detection Layer is mainly achieved through custom layout

Custom Widgets: Not all widgets in Flutter have a corresponding renderObejct that can be inherited

SingleChildRenderObjectWidget class to create a custom widget class VisibilityDetector extends SingleChildRenderObjectWidget {const VisibilityDetector({ required Key key, required Widget child, required this.onVisibilityChanged, }) final VisibilityChangedCallback? onVisibilityChanged; RenderVisibilityDetector createRenderObject(BuildContext context) { return RenderVisibilityDetector( key: key! , onVisibilityChanged: onVisibilityChanged, ); } void updateRenderObject( BuildContext context, RenderVisibilityDetector renderObject) { renderObject.onVisibilityChanged = onVisibilityChanged; }}Copy the code

In Flutter, the final rendering behavior is done with the renderObject. In this case, the custom renderObject is returned in the createRenderObject

Customize the paint method

class RenderVisibilityDetector extends RenderProxyBox {
  RenderVisibilityDetector({
    RenderBox? child,
    required this.key,
    required VisibilityChangedCallback? onVisibilityChanged,
  })   : _onVisibilityChanged = onVisibilityChanged,
        super(child);
  final Key key;
  VisibilityChangedCallback? _onVisibilityChanged;
  
  set onVisibilityChanged(VisibilityChangedCallback? value) {
    _onVisibilityChanged = value;
    markNeedsCompositingBitsUpdate();
    markNeedsPaint();
  }

  void paint(PaintingContext context, Offset offset) {
    final layer = VisibilityDetectorLayer(
        key: key,
        widgetSize: semanticBounds.size,
        paintOffset: offset,
        onVisibilityChanged: onVisibilityChanged!);
    context.pushLayer(layer, super.paint, offset);
  }
}
Copy the code

When the onVisibilityChanged property is set, the renderObject is marked as dirty, meaning that it needs to be redrawn, and the custom Layer is returned when the paint method is called

Custom layer in the layer method to implement the relevant layer visual range detection, and notify the outer layer

class VisibilityDetectorLayer extends ContainerLayer { VisibilityDetectorLayer( {required this.key, required this.widgetSize, required this.paintOffset, bool canRepeatReport = true, required this.onVisibilityChanged}) _layerOffset = Offset.zero; static Timer? _timer; static final _activeLayers = <Key, VisibilityDetectorLayer>{}; static final _updatedKeys = <Key>{}; static final _lastVisibility = <Key, VisibilityInfo>{}; bool filter = false; Bool canRepeatReport = true; // Whether the layer allows the final Key Key to be reported repeatedly. final Size widgetSize; Offset _layerOffset; final Offset paintOffset; final VisibilityChangedCallback onVisibilityChanged; void _scheduleUpdate() { _updatedKeys.add(key); _scheduleCallbacks(); } static void _scheduleCallbacks() { final updateInterval = VisibilityDetectorController.instance.updateInterval; if (_timer == null) { _timer = Timer(updateInterval, _handleTimer); } else { assert(_timer! .isActive); } } static void _handleTimer() { _timer = null; VisibilityDetectorController.exposureTimeMap.forEach((key, exposureLayer) { if (! _updatedKeys.contains(key)) { _updatedKeys.add(key); }}); SchedulerBinding.instance! .scheduleTask<void>(_processCallbacks, Priority.touch); } static void _processCallbacks() { for (final key in _updatedKeys) { final layer = _activeLayers[key]; if (layer ! = null) { layer._fireCallback(force: false); } } _updatedKeys.clear(); } void _fireCallback({required bool force}) { late VisibilityInfo info; if (! attached) { _activeLayers.remove(key); info = VisibilityInfo( key: key, size: _lastVisibility[key]? .size, ); } else { final widgetBounds = _computeWidgetBounds(); info = VisibilityInfo.fromRects( key: key, widgetBounds: widgetBounds, clipRect: _computeClipRect(), ); } final oldInfo = _lastVisibility[key]; final visible = ! info.visibleBounds.isEmpty && info.visibleFraction > VisibilityDetectorController.exposureFraction; if (oldInfo == null) { if (! Visible) {// if oldInfo is null, return (visible) {// if oldInfo is null, return (visible); }} // Whether the conditions are met bool isFirstReport =! filter; Reported for the first time / / / / can meet the requirements of reporting bool canReport = (isFirstReport | | (filter && canRepeatReport && oldInfo = = null)) && visible; int nowTime = new DateTime.now().millisecondsSinceEpoch; ExposureTimeLayer? prevLayer = VisibilityDetectorController.exposureTimeMap[key]; if (visible) { _lastVisibility[key] = info; if (canReport) { if (prevLayer ! = null && prevLayer.time > 0) { if ((nowTime - prevLayer.time) > VisibilityDetectorController.exposureTime) { filter = true; onVisibilityChanged(info); } else { VisibilityDetectorController.exposureTimeMap[key]? .layer = this; _scheduleCallbacks(); } } else { VisibilityDetectorController.exposureTimeMap[key] = ExposureTimeLayer(nowTime, this); _scheduleCallbacks(); } } } else { _lastVisibility.remove(key); _scheduleCallbacks(); } } void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) { _layerOffset = layerOffset; _scheduleUpdate(); super.addToScene(builder, layerOffset); } void attach(Object owner) { _activeLayers[key] = this; super.attach(owner); _scheduleUpdate(); } void detach() { super.detach(); _scheduleUpdate(); }}Copy the code

_activeLayers: Record Layer updatedKeys visible on the current screen: Record the key of the Layer that is currently updated. If Layer attach or detach is not used, add the key to the collection _lastVisibility: Record the set of visible layer keys on the screen. When the layer becomes invisible, the corresponding key VisibilityInfo should be removed from the set. The position information of the layer, including its size, and the visible area of the screen _fireCallback: Handle the logic of exposure reporting

Set a timer to add a detection task to the Dart event list at regular intervals

Dart’s event scheduling phases are: TransientCallbacks handle animation calculations, MidFrameMicrotasks handles build/ Layout /paint that is triggered by the transientCallbacks phase PostFrameCallbacks mainly before the next Frame, to do some preparations for the cleanup or idle does not produce Frame idle period, that can handle the Tasks (by SchedulerBinding. ScheduleTask trigger), Microtasks (triggered by scheduleMicrotask), the timer callback, response event processing (for example: the user’s input) details may refer to: segmentfault.com/a/119000001…