Flutter - 秒杀1/2曝光统计 📊

欢迎关注微信公众号:FSA全栈行动 👋

系列文章

开源库: flutter_scrollview_observer

  1. Flutter - 获取ListView当前正在显示的Widget信息
  2. Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
  3. Flutter - 快速实现聊天会话列表的效果,完美💯
  4. Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
  5. Flutter - 瀑布流交替播放视频 🎞
  6. Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
  7. Flutter - 滚动视图中的表单防遮挡 🗒

一、概述

在众多的曝光统计计算方式中,有这么一种特殊的,名为 1/2 曝光统计的计算方式,顾名思义就是模块露出的大小超过自身大小的 50% 时,需要触发一次统计,并记录起来防止被反复统计,当少于 50% 时会将曝光记录进行重置。

一般会用于统计在列表页中投放的广告和详情页中某些广告模块的曝光。

二、解决方案

要实时监测并计算来得到当前所有 item 的自身显示占比还是比较麻烦的,所以普遍我们会优先去找和使用已存在的解决方案。

那想必大家脑海中第一个想到的便是谷歌自家的 visibility_detector,这个库也确实很好用,常规场景下呢我也比较推荐大家使用它的,因为它真的太方便了!不过我所遇到的一种场景它却无法胜任,那就是在 CustomScrollView 中存在 SliverPersistentHeader 的情况,它的计算结果会不准确,如下所示。

注意看蓝色的 Middle Sliver 视图,当它刚被 AppBar 挡住时自身显示占比还是 1,直到超出了屏幕的上方才开始发生变化,当被 AppBar 完全遮挡时值为 0.58~

不过这里我着重介绍一下另一个方案,那就是使用我这个库(flutter_scrollview_observer) 去快速获取 item 自身显示占比,而且不会有上述的 bug

三、实战

ListView 为例,代码如下:

dart 复制代码
final observerController = ListObserverController();

ListViewObserver(
  child: _buildListView(),
  // 不进行对比,直接把结果返出来
  triggerOnObserveType: ObserverTriggerOnObserveType.directly,
  controller: observerController,
  onObserve: (resultModel) {
    // 从观察结果中拿到正在展示的所有 item 的数据
    final models = resultModel.displayingChildModelList;
    // 取出所有下标
    final indexList = models.map((e) => e.index).toList();
    // 取出所有 item 的自身显示占比
    final displayPercentageList =
        models.map((e) => e.displayPercentage).toList();
    debugPrint('index -- $indexList -- $displayPercentageList');
  },

是的,拿到所有的 item 的自身显示占比就是这么简单,通过使用对应的 WidgetObserver 去对滚动视图进行观察就可以了。

每次滚动的时候就会直接返回观察结果,如果需要在不滚动的时候也能进行一次观察,可以调用如下方法

dart 复制代码
observerController.dispatchOnceObserve();

四、统计逻辑

上面你已经能拿到自身显示占比的数据,那接下来就可以做是否触发曝光的逻辑判断了,这个比较业务化,所以这里直接给出我的代码吧,供参与使用

dart 复制代码
import 'package:scrollview_observer/scrollview_observer.dart';

mixin VisibilityExposureMixin {
  // 记录 item 已曝光的 Map
  Map<dynamic, bool> exposureRecordMap = {};

  /// 重置所有 item 的曝光记录
  resetExposureRecordMap() {
    exposureRecordMap.clear();
  }
  
  /// 处理滚动视图中 item 的曝光
  /// 
  /// [resultModel] 监听结果(基类是 ObserveModel, 传 onObserve 回调中的值,或 onObserveAll 中根据 BuildContext 取出来的值)
  /// [toExposeDisplayPercent] 当自身显示占比超过该值时视为曝光且记录起来,否则重置曝光记录
  /// [recordKeyCallback] 返回用于记录 item 已曝光的 key,不实现则使用下标
  /// [needExposeCallback] 用于确定对应下标的 item 是否参与曝光计算逻辑,不实现则为 true
  /// [toExposeCallback] 满足曝光条件后的回调
  handleExposure({
    required dynamic resultModel,
    double toExposeDisplayPercent = 0.5,
    dynamic Function(int index)? recordKeyCallback,
    bool Function(int index)? needExposeCallback,
    required Function(int index) toExposeCallback,
  }) {
    List<ObserveDisplayingChildModelMixin> displayingChildModelList = [];
    if (resultModel is ListViewObserveModel) {
      displayingChildModelList = resultModel.displayingChildModelList;
    } else if (resultModel is GridViewObserveModel) {
      displayingChildModelList = resultModel.displayingChildModelList;
    }
    for (var displayingChildModel in displayingChildModelList) {
      final index = displayingChildModel.index;
      final recordKey = recordKeyCallback?.call(index) ?? index;
      // 让外部告诉我们 index 对应的 item 是否需要参与曝光计算逻辑
      final needExpose = needExposeCallback?.call(index) ?? true;
      if (!needExpose) continue;
      // debugPrint('item : $index - ${displayingChildModel.displayPercentage}');
      // 判断 item 自身显示占比是否超过 [toExposeDisplayPercent]
      if (displayingChildModel.displayPercentage < toExposeDisplayPercent) {
        // 不满足曝光条件,重置曝光记录
        exposureRecordMap[recordKey] = false;
      } else {
        // 满足暴露条件
        final haveExposure = exposureRecordMap[recordKey] ?? false;
        if (haveExposure) continue;
        toExposeCallback(index);
        exposureRecordMap[recordKey] = true;
      }
    }
  }
}

逻辑:

  1. 达到 1/2 后触发曝光统计,并记录起来,防触发多次请求
  2. 小于 1/2 时重置当前 item 的曝光记录

使用:

混入 VisibilityExposureMixin

dart 复制代码
class _VisibilityListViewPageState extends State<VisibilityListViewPage>
    with VisibilityExposureMixin {
  ...
}

onObserve 中调用 handleExposure 方法

dart 复制代码
onObserve: (resultModel) {
  handleExposure(
    resultModel: resultModel,
    needExposeCallback: (index) {
      // 只有下标为 6 的 item 需要计算是否曝光
      return index == 6;
    },
    toExposeCallback: (index) {
      // 满足条件,可以上报曝光了
      debugPrint('Exposure -- $index');
    },
  );
},

最终看下效果吧,注意看红色视图和控制台的输出

Demo链接:visibility_demo

五、最后

通过上述示例的讲解,相信你对 scrollview_observer 的使用又更加清楚,如果你也觉得这个库好用,请不吝给个 Star 👍

GitHub: github.com/LinXunFeng/...

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho5 小时前
【TypeScript】知识点梳理(三)
前端·typescript