Flutter - 秒杀1/2曝光统计 📊
# 一、概述
在众多的曝光统计计算方式中,有这么一种特殊的,名为 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/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
- 01
- Flutter - 子部件任意位置观察滚动数据11-24
- 02
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 03
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21