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 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
学习路上_write6 分钟前
FPGA/Verilog,Quartus环境下if-else语句和case语句RT视图对比/学习记录
单片机·嵌入式硬件·qt·学习·fpga开发·github·硬件工程
喵叔哟8 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js