Android recyclerView曝光统计

一、背景

产品需求中我们经常会有统计recyclerView的每个item的曝光需求:
recyclerView上下滚动每个item从不可见进入到屏幕可见范围(这里包含item的可见范围,还有item的曝光时长)
在tab切换,或者页面切换的时候会引起recyclerView从不可见到可见的变化(当前屏幕上可见的item都算一次曝光)
数据变化引起的曝光
为了达到产品需求,我们首先需要数据收集,在滑动过程中收集所有需要上报的item,然后在适当的时机进行上报比如滑动停止、页面切换。

二、需求分析

通过上面的需求分析,我们可以知道recyclerView的曝光主要分为滑动曝光和可见性变化曝光,还有数据变化曝光。
1、滑动曝光
我们可以通过监听recyclerView的滑动过程,在滑动的过程中收集曝光的数据(因为曝光行为就是在滑动过程中产生的),然后当滑动停止的时候去进行一个曝光上报(这样既能保证实时性,又可以兼顾手机的性能)。

2、可见性变化曝光
这里我们需要监听recyclerView的可见性变化,但是并没有提供给我们View可见性变化的监听。虽然有一些焦点变化的监听,但是并不能完全覆盖View的可见性变化。所以这里我们必须想别的办法来实现,这里我通过Actvity的生命周期的onResume和onPause想到有没有可能实现Fragment的onFragmentResume和onFragmentPause来监听Fragment的可见性,监听到Fragment的可见性,也就相当于监听到recyclerView的可见性。然后遍历当前可见的Item收集,并上报即可。

3、数据变化引起的曝光
有时候数据变化也会引起相应的曝光,这种的我们比较好处理只需要监听相应的数据变化。然后对可见的需要曝光的item进行曝光处理即可。

三、实现recyclerView滑动过程中引起的曝光

实现原理流程图如下:
在这里插入图片描述
因为Adatper控制着RecyclerView的ViewHolder的创建和绑定,并且对应的数据适配都是在Adapter中完成的,所以这里选择重写Adapter来实现曝光功能。

首先我们需要在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列表当中

为什么这个筛选过程要放在onScrolled过程中?
首先曝光的行为是在滑动过程中达成的,比如我们不断的上下滑动recyclerView,导致item_1不断的出现在屏幕中和消失在屏幕中。假如这个过程中item_1曝光了5次,滑动停止后回到我们初始的滑动位置。如果我们在滑动停止的时候来筛选曝光的item,可能会认为完全没有新曝光的item。因为我们滑动停止在了原来初始的位置,显然这个计算是不对的。想要正确的记录曝光的item,就必须要在recyclerView的滑动过程中去筛选达到曝光条件的item。
其次考虑到曝光时间,在滑动过程中Item达到曝光条件,这时候我们就应该记录曝光的开始时间。在其他的时机无法正确的记录曝光时间。

在onScrolled的过程中进行筛选计算是否会影响recyclerView的性能,导致滑动不流畅?
这个筛选计算分为两个部分,一个部分是需要开发者定义的筛选逻辑,这里就需要开发者自己注意不要有耗时的判断逻辑。 第二个部分是内部的筛选逻辑,主要是判断item的露出高度是否达到曝光要求。
这个判断是否会影响recyclerView的性能?其实也是不会的。
首先item的自身的高度、位置(滑动偏移量)在滑动过程中每一帧的渲染之前都是已经由recyclerView计算好的,否则recyclerView也没有办法把每个item绘制在正确的位置。所以显然这个计算肯定不会影响recyclerView的流畅性。
而我们需要做的判断主要是拿到当前item的位置信息,进行比较看是否达到曝光要求,这个显然也不是一个耗时操作。我们是通过 itemView!!.getGlobalVisibleRect(rect)这个方法来获取item的位置信息的,通过代码跟踪,我们可以看下具体实现逻辑

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;
}

通过代码可知我们获取itemView的可见范围主要是通过itemView的当前位置rect和他所在的ViewGroup(即recyclerView)的范围大小做交集。看代码可知也都是一些比较大小的逻辑,没有耗时操作,所以无需担心这个会造成recyclerView的滑动卡顿。
那么接下来我们看下具体实现筛选过程

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)
            }
            it.remove()
        }
    }
}

onScrolled方法第8-10行是更新曝光开始时间 onScrolled方法第7行是露出范围的检测

/**
 * 内部判断当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
}

onScrolled方法第11-16行是我们自定义的筛选条件判断,即下面对应第10行的筛选条件

/**
  * 设置曝光监听
  */
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 -> {
 }

第5行就是我们对曝光时长的筛选

/**
 * 内部判断当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
}

在ViewHolder AttachToWindow 的时候,和RecyclerView 滑动的时候我们会更新曝光开始时间,在 ViewHolder DettachToWindw 和 RecyclerView 滑动停止的时候我们会更新曝光结束时间。
item.endTime 不为零如果达到曝光时长,则表示需要进行曝光加入曝光列表,否则就舍弃。
item.endTime 为零则表示ViewHoler还在持续曝光,则用当前时间计算,达到曝光时间加入曝光列表,否则不做处理(因为还在持续曝光,等下次滑动达到曝光时长在进行曝光)。
下面是onViewDetachedFromWindow的具体代码,包括移除无需曝光的数据和更新曝光结束时间。

/**
 * 对移除的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在滑动过程中引起的上报。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网小熊猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值