一、概要
100行代碼實(shí)現(xiàn)recyclerview條目曝光埋點(diǎn)設(shè)計(jì)
二、設(shè)計(jì)思路
- 條目露出來一半以上視為該條目曝光。
- 在rv滾動(dòng)過程中或者數(shù)據(jù)變更回調(diào)OnGlobalLayoutListener時(shí),將符合條件1的條目記錄在曝光列表、上傳埋點(diǎn)集合里。
- 滾動(dòng)狀態(tài)變更和OnGlobalLayoutListener回調(diào)時(shí),且列表狀態(tài)為idle狀態(tài),觸發(fā)上報(bào)埋點(diǎn)。
三、容錯(cuò)性
- 滑動(dòng)過快時(shí),視為未曝光
- 數(shù)據(jù)變更時(shí),重新檢測曝光
- 曝光過的條目,不會(huì)重復(fù)曝光
四、接入影響
- 對業(yè)務(wù)代碼零侵入
- 對列表滑動(dòng)體驗(yàn)無影響
五、代碼實(shí)現(xiàn)
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import java.util.*
class RVItemExposureListener(
private val mRecyclerView: RecyclerView,
private val mExposureListener: IOnExposureListener?
) {
interface IOnExposureListener {
fun onExposure(position: Int)
fun onUpload(exposureList: List<Int>?): Boolean
}
private val mExposureList: MutableList<Int> = ArrayList()
private val mUploadList: MutableList<Int> = ArrayList()
private var mScrollState = 0
var isEnableExposure = true
private var mCheckChildViewExposure = true
private val mViewVisible = Rect()
fun checkChildExposeStatus() {
if (!isEnableExposure) {
return
}
val length = mRecyclerView.childCount
if (length != 0) {
var view: View?
for (i in 0 until length) {
view = mRecyclerView.getChildAt(i)
if (view != null) {
view.getLocalVisibleRect(mViewVisible)
if (mViewVisible.height() > view.height / 2 && mViewVisible.top < mRecyclerView.bottom) {
checkExposure(view)
}
}
}
}
}
private fun checkExposure(childView: View): Boolean {
val position = mRecyclerView.getChildAdapterPosition(childView)
if (position < 0 || mExposureList.contains(position)) {
return false
}
mExposureList.add(position)
mUploadList.add(position)
mExposureListener?.onExposure(position)
return true
}
private fun uploadList() {
if (mScrollState == RecyclerView.SCROLL_STATE_IDLE && mUploadList.size > 0 && mExposureListener != null) {
val success = mExposureListener.onUpload(mUploadList)
if (success) {
mUploadList.clear()
}
}
}
init {
mRecyclerView.viewTreeObserver.addOnGlobalLayoutListener {
if (mRecyclerView.childCount == 0 || !mCheckChildViewExposure) {
return@addOnGlobalLayoutListener
}
checkChildExposeStatus()
uploadList()
mCheckChildViewExposure = false
}
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
super.onScrollStateChanged(recyclerView, newState)
mScrollState = newState
uploadList()
}
override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int
) {
super.onScrolled(recyclerView, dx, dy)
if (!isEnableExposure) {
return
}
// 大于50視為滑動(dòng)過快
if (mScrollState == RecyclerView.SCROLL_STATE_SETTLING && Math.abs(dy) > 50) {
return
}
checkChildExposeStatus()
}
})
}
}
六、使用
RVItemExposureListener(yourRecyclerView, object : RVItemExposureListener.IOnExposureListener {
override fun onExposure(position: Int) {
// 滑動(dòng)過程中出現(xiàn)的條目
Log.d("exposure-curPosition:", position.toString())
}
override fun onUpload(exposureList: List<Int>?): Boolean {
Log.d("exposure-positionList", exposureList.toString())
// 上報(bào)成功后返回true
return true
}
})
完結(jié),撒花??