本文字数:6661字
预计阅读时间:17分钟
本文提供了一种Android列表曝光统计的功能实现。使开发者无需关心数据收集过程,只需简单的设置即可在适当的曝光时机通知开发者所需的曝光数据。
产品需求中我们经常会有统计recyclerView的每个item的曝光需求:
recyclerView上下滚动每个item从不可见进入到屏幕可见范围(这里包含item的可见范围,还有item的曝光时长)
在tab切换,或者页面切换的时候会引起recyclerView从不可见到可见的变化(当前屏幕上可见的item都算一次曝光)
数据变化引起的曝光
为了达到产品需求,我们首先需要数据收集,在滑动过程中收集所有需要上报的item,然后在适当的时机进行上报比如滑动停止、页面切换。
我们希望开发者在使用的时候,不需要自己关心数据收集,只需要注册了对应的曝光回调,就可以在适当的时机返回需要上报的item数据。
1 使用方法
//设置露出多少算曝光
myAdapter.setOutPercent(0.1f);
//设置显示多长时间算曝光
myAdapter.setExposureTime(0);
//设置曝光监听
myAdapter.setExposureFunc(items->{
//返回需要曝光的数据列表
for (ExpItem<NewFeedBean> item : items) {
LogUtil.d("kami","exposure position = " + item.getPostion() + ": " +item.getData().sourceFeed.content + ",duration = " + (item.getEndTime() - item.getStartTime()));
}
return null;
},item->{
//自定义需要曝光筛选:比如只曝光广告数据
return item.getData().isAd();
});
这里我们会返回一个ExpItem列表即需要曝光的数据,开发者只需要在这里写对应的上报逻辑即可。
ExpItem包含数据如下:
class ExpItem<T> {
//对应item的itemView
var itemView: View? = null
//所在位置
var postion: Int = 0
//对应item的关联数据
var data: T? = null
//开始曝光时间
var startTime = 0L
//结束曝光时间
var endTime = 0L
}
2 解决方案
通过上面的需求分析,我们可以知道recyclerView的曝光主要分为滑动曝光和可见性变化曝光,还有数据变化曝光。
所以接下来我们也分成这三个部分去分别实现:
1 滑动曝光
我们可以通过监听recyclerView的滑动过程,在滑动的过程中收集曝光的数据(因为曝光行为就是在滑动过程中产生的),然后当滑动停止的时候去进行一个曝光上报(这样既能保证实时性,又可以兼顾手机的性能)。
2 可见性变化曝光
这里我们需要监听recyclerView的可见性变化,但是并没有提供给我们View可见性变化的监听。虽然有一些焦点变化的监听,但是并不能完全覆盖View的可见性变化。所以这里我们必须想别的办法来实现,这里我通过Actvity的生命周期的onResume和onPause想到有没有可能实现Fragment的onFragmentResume和onFragmentPause来监听Fragment的可见性,监听到Fragment的可见性,也就相当于监听到recyclerView的可见性。然后遍历当前可见的Item收集,并上报即可。
3 数据变化引起的曝光
有时候数据变化也会引起相应的曝光,这种的我们比较好处理只需要监听相应的数据变化。然后对可见的需要曝光的item进行曝光处理即可。
接下来将从三个方面分析具体的实现原理。
3 实现原理
因为Adatper控制着RecyclerView的ViewHolder的创建和绑定,并且对应的数据适配都是在Adapter中完成的,所以这里选择重写Adapter来实现曝光功能。
3.1 recyclerView滑动过程中引起的曝光
首先我们需要在recyclerView的滑动过程中进行数据收集,即收集显示到屏幕上需要曝光的item。
我们知道当recylerView的ViewHolder加载到屏幕上会先调用onViewAttachedToWindow(holder: VH),所以我们就选择在这个方法进行数据收集。只要显示到屏幕的数据都会被收集到collectDatas列表当中
/**
* 进行曝光items收集
*/
override fun onViewAttachedToWindow(holder: VH) {
val item = ExpItem<T>()
item.data = holder.mData
item.itemView = holder.itemView
item.postion = holder.mPosition
collectDatas.add(item)
super.onViewAttachedToWindow(holder)
//检查曝光范围,并更新曝光开始时间
if (innerCheckExposureData(item)){
item.startTime = TimeUtil.getCurrentTimeMillis()
}
}
接着我们需要筛选需要进行曝光的数据,计算每个Item在屏幕上的位置,自定义筛选条件(比如:只曝光广告)这个筛选我们需要在recyclerView的滚动过程中进行计算,因为滚动过程中ViewHolder的露出范围是不断发生改变的,然后我们把筛选的数据从collectDatas中移动到expDatas列表当中
1.为什么这个筛选过程要放在onScrolled过程中?
首先曝光的行为是在滑动过程中达成的,比如我们不断的上下滑动recyclerView,导致item_1不断的出现在屏幕中和消失在屏幕中。假如这个过程中item_1曝光了5次,滑动停止后回到我们初始的滑动位置。如果我们在滑动停止的时候来筛选曝光的item,可能会认为完全没有新曝光的item。因为我们滑动停止在了原来初始的位置,显然这个计算是不对的。想要正确的记录曝光的item,就必须要在recyclerView的滑动过程中去筛选达到曝光条件的item。
其次考虑到曝光时间,在滑动过程中Item达到曝光条件,这时候我们就应该记录曝光的开始时间。在其他的时机无法正确的记录曝光时间。
2.在onScrolled的过程中进行筛选计算是否会影响recyclerView的性能,导致滑动不流畅?
这个筛选计算分为两个部分,一个部分是需要开发者定义的筛选逻辑,这里就需要开发者自己注意不要有耗时的判断逻辑。 第二个部分是内部的筛选逻辑,主要是判断item的露出高度是否达到曝光要求。
这个判断是否会影响recyclerView的性能?其实也是不会的。
首先item的自身的高度、位置(滑动偏移量)在滑动过程中每一帧的渲染之前都是已经由recyclerView计算好的,否则recyclerView也没有办法把每个item绘制在正确的位置。所以显然这个计算肯定不会影响recyclerView的流畅性。
而我们需要做的判断主要是拿到当前item的位置信息,进行比较看是否达到曝光要求,这个显然也不是一个耗时操作。我们是通过 itemView!!.getGlobalVisibleRect(rect)这个方法来获取item的位置信息的,通过代码跟踪,我们可以看下具体