FSA全栈行动 FSA全栈行动
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr

公众号:FSA全栈行动

记录学习过程中的知识
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr
  • AndroidUI

  • Android第三方SDK

  • Android混淆

  • Android仓库

  • Android新闻

  • Android系统开发

  • Android源码

  • Android注解AOP

  • Android脚本

  • AndroidTv开发

  • AndroidNDK

  • Android音视频

  • Android热修复

  • Android性能优化

  • Android云游戏

  • Android插件化

  • iOSUI

  • iOS工具

  • iOS底层原理与应用

  • iOS组件化

  • iOS音视频

  • iOS疑难杂症

  • iOS之Swift

  • iOS之RxSwift

  • iOS开源项目

  • iOS逆向

  • Flutter开发

    • Dart - 抽象类的实例化
    • Flutter - 打印好用的Debug日志
    • Flutter - 混合开发
    • Flutter - 解决混合开发iOS脚本打包遇到的问题
    • Flutter - 低版本在iOS14上遇到的问题与解决方案
    • Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
    • Flutter - 实现列表上下拉切换header
    • Flutter - 获取ListView当前正在显示的Widget信息
    • Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
    • Flutter - 快速实现聊天会话列表的效果,完美💯
    • Flutter - 聊天输入框更新文本时的必备优化点🔖
    • Flutter - 我给官方提PR,解决run命令卡住问题 😃
    • Flutter - 探索run命令到底做了什么 🤔
    • Flutter - 引擎调试(iOS篇)🛠
    • Flutter - 引擎调试bug到提交PR实战 🐞
    • Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
    • Flutter - 瀑布流交替播放视频 🎞
    • Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
    • Flutter - 滚动视图中的表单防遮挡 🗒
    • Flutter - 秒杀1/2曝光统计 📊
      • 一、概述
      • 二、解决方案
      • 三、实战
      • 四、统计逻辑
    • 一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐
    • Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
    • Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)
    • Flutter - 混编项目集成Shorebird热更新🐦(iOS篇)
    • Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞
    • Flutter - 升级3.19之后页面多次rebuild?🤨
    • Flutter - 热更新 Shorebird 1.0 正式版来了 🐦
    • Flutter - 使用Pigeon实现视频缓存插件 🐌
    • Flutter - 轻松搞定屏幕旋转功能 😎
    • Flutter - 解决Connection closed before full header was received
    • Flutter - 实现聊天键盘与功能面板的丝滑切换 🍻
    • Flutter - 支持观察NestedScrollView,兼容性更强 😈
    • Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻
    • Flutter - 升级到3.24后页面还会多次rebuild吗?🧐
    • Flutter - 轻松实现PageView卡片偏移效果
    • Flutter - 轻松搞定炫酷视差(Parallax)效果
    • Flutter - 危!3.24版本苹果审核被拒!
    • Flutter - 子部件任意位置观察滚动数据
    • Flutter - iOS编译加速
    • Flutter - Xcode16 还原编译速度
  • 移动端
  • Flutter开发
LinXunFeng
2023-08-26
目录

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

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

# 一、概述

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

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

# 二、解决方案

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

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

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

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

# 三、实战

以 ListView 为例,代码如下:

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 去对滚动视图进行观察就可以了。

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

observerController.dispatchOnceObserve();

# 四、统计逻辑

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

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

class _VisibilityListViewPageState extends State<VisibilityListViewPage>
    with VisibilityExposureMixin {
  ...
}

在 onObserve 中调用 handleExposure 方法

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

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

曝光左下角 ListView 中下标为 6 的红色 item

曝光左下角 SliverGrid 中下标为 6 的紫色 item

Demo链接:visibility_demo (opens new window)

#Dart#Flutter
Flutter - 滚动视图中的表单防遮挡 🗒
一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐

← Flutter - 滚动视图中的表单防遮挡 🗒 一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐→

最近更新
01
Flutter - Xcode16 还原编译速度
04-05
02
AI - 免费的 Cursor 平替方案
03-30
03
Android - 2025年安卓真的闭源了吗
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2025 FSA全栈行动
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×