RecyclerView的曝光统计
本文字数:6661字
预计阅读时间:17分钟
recyclerView上下滚动每个item从不可见进入到屏幕可见范围(这里包含item的可见范围,还有item的曝光时长) 在tab切换,或者页面切换的时候会引起recyclerView从不可见到可见的变化(当前屏幕上可见的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();
});
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 解决方案
1 滑动曝光
我们可以通过监听recyclerView的滑动过程,在滑动的过程中收集曝光的数据(因为曝光行为就是在滑动过程中产生的),然后当滑动停止的时候去进行一个曝光上报(这样既能保证实时性,又可以兼顾手机的性能)。
2 可见性变化曝光
这里我们需要监听recyclerView的可见性变化,但是并没有提供给我们View可见性变化的监听。虽然有一些焦点变化的监听,但是并不能完全覆盖View的可见性变化。所以这里我们必须想别的办法来实现,这里我通过Actvity的生命周期的onResume和onPause想到有没有可能实现Fragment的onFragmentResume和onFragmentPause来监听Fragment的可见性,监听到Fragment的可见性,也就相当于监听到recyclerView的可见性。然后遍历当前可见的Item收集,并上报即可。
3 数据变化引起的曝光
有时候数据变化也会引起相应的曝光,这种的我们比较好处理只需要监听相应的数据变化。然后对可见的需要曝光的item进行曝光处理即可。
接下来将从三个方面分析具体的实现原理。
3 实现原理
3.1 recyclerView滑动过程中引起的曝光
我们知道当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过程中?
2.在onScrolled的过程中进行筛选计算是否会影响recyclerView的性能,导致滑动不流畅?
public boolean getChildVisibleRect(
View child, Rect r, android.graphics.Point offset, boolean forceParentCheck) {
...
rect.set(r);
final int dx = child.mLeft - mScrollX;
final int dy = child.mTop - mScrollY;
...
rect.offset(dx, dy);
...
rectIsVisible = rect.intersect(0, 0, width, height);
...
return rectIsVisible;
}
public boolean intersect(float left, float top, float right, float bottom) {
if (this.left < right && left < this.right
&& this.top < bottom && top < this.bottom) {
if (this.left < left) {
this.left = left;
}
if (this.top < top) {
this.top = top;
}
if (this.right > right) {
this.right = right;
}
if (this.bottom > bottom) {
this.bottom = bottom;
}
return true;
}
return false;
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val it = collectDatas.iterator()
while (it.hasNext()) {
val item = it.next()
//判断露出范围
if (innerCheckExposureData(item)) {
if (item.startTime == 0L) {
item.startTime = TimeUtil.getCurrentTimeMillis()
}
if (funcCheck == null) {
expDatas.add(item)
//自定义过滤条件
} else if (funcCheck!!.invoke(item)) {
expDatas.add(item)
}
// LogUtil.d("kami", "scroll remove collectDatas to expDatas = " + item.postion)
it.remove()
}
}
}
/**
* 内部判断当itemView的可见度达到多少才需要曝光
*/
private fun innerCheckExposureData(item: ExpItem<*>): Boolean {
val rect = Rect()
val visible = item.itemView!!.getGlobalVisibleRect(rect)
if (visible) {
if (rect.height() >= item.itemView!!.measuredHeight * outPercent) {
return true
}
}
return false
}
/**
* 设置曝光监听
*/
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();
});
最后在滑动停止进行曝光数据回调之前进行曝光时长的筛选。从expDatas中选择达到曝光时长的数据,最后进行数据上报
//设置曝光监听
when (newState) {
//滑动完成
RecyclerView.SCROLL_STATE_IDLE ->
val needExpDatas = getExposureList(expDatas)
if (!needExpDatas.isEmpty()) {
funcExp?.invoke(needExpDatas)
}
else -> {
}
/**
* 内部判断当itemView的曝光时长达到多少才需要进行曝光
*/
private fun getExposureList(expDatas: ArrayList<ExpItem<T>>): ArrayList<ExpItem<T>> {
val needExpDatas = ArrayList<ExpItem<T>>()
val it = expDatas.iterator()
while (it.hasNext()) {
val item = it.next()
if (item.endTime != 0L) {
if (item.endTime - item.startTime >= exposureTime) {
needExpDatas.add(item)
}
it.remove()
} else {
if (TimeUtil.getCurrentTimeMillis() - item.startTime >= exposureTime) {
item.endTime = TimeUtil.getCurrentTimeMillis()
needExpDatas.add(item)
it.remove()
}
}
}
return needExpDatas
}
/**
* 对移除的itemView进行曝光时长的修改
*/
override fun onViewDetachedFromWindow(holder: VH) {
// 在Dettached的时候,未被移动到expDatas列表的数据证明没有达到曝光条件,无需曝光。就可以把它们从collectDatas列表中移除了
val it = collectDatas.iterator()
while (it.hasNext()) {
val item = it.next()
if (holder .mPosition == item.postion && holder.itemView === item.itemView) {
it.remove()
}
}
// 更新曝光结束时间
for (expItem in expDatas) {
if (holder.mPosition == expItem.postion && expItem.itemView === holder.itemView) {
expItem.endTime = TimeUtil.getCurrentTimeMillis()
}
}
super.onViewDetachedFromWindow(holder)
}
这样我们就完成了RecyclerView在滑动过程中引起的上报。
3.2 切换页面,reyclerView从不可见到可见的曝光
上面我们完成了RecyclerView在滑动过程中的上报,接下来需要实现页面切换引起的曝光。 第一我们需要实现recyclerView从可见到不可见的变化监听,第二在RecyclerView从不可见到可见的时候,我们遍历recyclerView当前在屏幕中的ViewHoler进行曝光处理即可。
遍历RecyclerView获取可见ViewHolder我们很好实现,但是监听RecyclerView的可见性变化却是不怎么好实现的。
Activity跳转引起的页面切换,我们可以监听Activity的生命周期来监听,页面的可见性发生变化。
但是若是Fragment之间的tab切换,引起的可见性变化却是不好监听的。
我也尝试过用ViewTreeObserver.onWindowFocusChangeListener来监听,但是也只是在Activity跳转的时候才会引起的焦点发生变化,和监听Activity生命周期是一个效果。
所以这里我们必须要实现一个Fragment可见性发生变化的监听,这样在Fragment当中的RecyclerView就可以通过Fragment知道自己的可见性变化。
🔺3.2.1 Fragment可见性变化的监听
我们知道Activity从不可见到可见会回调 onResume ,从可见到不可见会回调onPause。而Fragment的生命周期是跟随Activity的,但是onResume和onPause并不能表示Fragment的完全表示可见性的变化。
我们判断Fragment的可见性发生了变化,比如tab切换 通过FragmentManager.show(fragment)去显示。这时候会回调onHiddenChanged(boolean hidden)表示Fragment的可见性发生变化。
还有 Fragment+ViewPage的方式,我们切换页面。但是并没有回调onHiddenChanged(boolean hidden)。这时候我们是通过setUserVisibleHint(boolean isVisibleToUser)的回调来判断Fragment的可见性发生变化。
总结:Fragment的可见性是通过
1.跟随Activity: onResume +onPause 2.被FragmentManger控制:onHiddenChanged(boolean hidden) 3.Fragment+ViewPager: setUserVisibleHint(boolean isVisibleToUser)
注:2和3是互斥的不会同时出现。 但是这样我们想要监听Fragment的可见性变化就需要根据具体的业务情况。各自处理,尤其是当Fragment的出现深层的父Fragment套子Fragment,这样处理起来就比较麻烦。比如下面的情况:
父Fragment | 子Fragment |
---|---|
可见→ 不可见 | 可见→ 不可见 ; 不可见 → 不可见 |
不可见 → 可见 | 不可见(部分 A1)→ 可见; 不可见(部分 A2) → 不可见 |
@Override
public void onResume() {
super.onResume();
if (fromVisibleHint) {
fakeVisible = getUserVisibleHint();
}else{
fakeVisible = !isHidden();
}
//当有父Fragment的时候,会跟随父Fragment的可见性变化,所以无需处理
if (getParentFragment() != null && getParentFragment() instanceof BaseFragment) {
return;
}
if (fakeVisible) {
isVisible = true;
onFragmentResume(true);
}
}
Fragment的onResume是跟随Actvity的onResume的,所以同样会引起Fragment可见性发生变化。我们首先获取当前Fragment的fakeVisible,然后判断假如该Fragment有父Fragment,那么直接return不做处理,因为对于子Fragment我们要让他跟随父Fragment的可见性变化。假如该Fragment没有父Fragment,我们认为fakeVisible所表达的可见性是正确的。只有真正可见的Fragment我们设置isVisible = true ,然后回调onFragmentResume。
那么有人要问了,假如这个Fragment有子的Fragment呢?
它的子Fragment如何处理?接着看下面代码:
/**
* Fragment可见状态恢复
*
* @param isActivityResume 是否是跟随Activity的OnResume行为
*/
@CallSuper
public void onFragmentResume(boolean isActivityResume) {
if (!isAdded()) {
return;
}
FragmentManager manager = getChildFragmentManager();
List<Fragment> fragments = manager.getFragments();
if (fragments != null) {
for (Fragment childFragment : fragments) {
if (childFragment != null && childFragment instanceof BaseFragment) {
if (((BaseFragment) childFragment).fakeVisible) {
((BaseFragment) childFragment).isVisible = true;
((BaseFragment) childFragment).onFragmentResume(isActivityResume);
}
}
}
}
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
onTabChanged(!hidden);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
fromVisibleHint = true;
onTabChanged(isVisibleToUser);
}
private void onTabChanged(boolean visible){
fakeVisible = visible;
boolean curVisible = fakeVisible;
if (getParentFragment() != null && getParentFragment() instanceof BaseFragment){
BaseFragment parent = (BaseFragment)getParentFragment();
if (parent.isVisible = false) {
curVisible = false;
}
}
if (curVisible && !isVisible) {
isVisible = true;
onFragmentResume(false);
} else if(!curVisible && isVisible){
isVisible = false;
onFragmentPause(false);
}
}
🔺3.2.2 实现recycleView页面切换曝光
private fun registLifecycle(){
//监听 fragment 或者 activity的生命周期,对可见item进行曝光
if (fragment!=null) {
LifecycleUtil.bindLifecycle(fragment,{
if (it == Constants.ActivityEvent.RESUME) {
exposureVisible()
}
})
}else if(mContext is FragmentActivity) {
LifecycleUtil.bindObserver(mContext as FragmentActivity,{
if (it == Constants.ActivityEvent.RESUME) {
exposureVisible()
}
})
}
}
/**
* 曝光可见items
*/
private fun exposureVisible(){
val needExpDatas = getVisibleExposureList()
if (!needExpDatas.isEmpty()) {
funcExp?.invoke(needExpDatas)
}
}
/**
* 获取当前可见itemViews的曝光列表
*/
private fun getVisibleExposureList(): ArrayList<ExpItem<T>>{
var exposureList = arrayListOf<ExpItem<T>>()
for(index in 0..mRecyclerView.childCount-1){
var itemView = mRecyclerView.getChildAt(index)
itemView?.let {
var vh = mRecyclerView.getChildViewHolder(it)
if (vh is HyBaseViewHolder<*>) {
val item = ExpItem<T>()
item.data = vh.mData as T
item.itemView = vh.itemView
item.postion = vh.mPosition
if (innerCheckExposureData(item)) {
if (funcCheck==null) {
exposureList.add(item)
}else if (funcCheck!!.invoke(item)) {
exposureList.add(item)
}
if (collectDatas.contains(item)) {
collectDatas.remove(item)
}
}
}
}
}
return exposureList
}
3.3 RecylerView数据改变引起的曝光
registerAdapterDataObserver(object :RecyclerView.AdapterDataObserver(){
override fun onChanged() {
super.onChanged()
exposureVisible()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
val needExpDatas = getVisibleExposureList(position,itemCount)
if (!needExpDatas.isEmpty()) {
funcExp?.invoke(needExpDatas)
}
}
})
4 总结
也许你还想看
(▼点击文章标题或封面查看)
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛