RecyclerView曝光item埋点 RecyclerView的Child展现监听实现方法

PM:我给你提个需求,卡片展示在界面上后,我要你上报数据给我。

AeD0v

简单做法

//添加监听
recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
    //item被添加到视图上
    override fun onChildViewAttachedToWindow(view: View) {
        recyclerView.getChildLayoutPosition(view)//取到对应的position
            .takeIf { it in adapter.currentList.indices }?.let {//不越界就进行操作
                //实现对应的逻辑
            }
    }

    //item被移除到视图上
    override fun onChildViewDetachedFromWindow(view: View) {

    }
})
复制代码

PM:你这才展示了一点点就上报了,我想要展示了50%才上报。

AeD0v

支持展示比例设置的方法

之前那种方法的回调不支持展示比例的修改,但是我们可以给recyclerview添加OnScrollListener 然后用户滚动的时候去判断item的展示


class ItemShowDetector(val recyclerView: RecyclerView, val onShow: (position: Int) -> Unit) : RecyclerView.OnScrollListener() {

    /**
     * 可见百分比 0-100
     */
    var visiblePercent = 50
 

    /**
     * 保存曝光的状态
     */
    var flag: BooleanArray = BooleanArray(0)


    private val adapter: RecyclerView.Adapter<*> =
        if (recyclerView.adapter == null) throw RuntimeException("recyclerview未设置adapter") else recyclerView.adapter!!

    init {
        //监听滚动监听
        recyclerView.addOnScrollListener(this)
 

        //监控adapter的数据变化
        adapter.registerAdapterDataObserver(DataObserver())

        //检测初始化的曝光
        recyclerView.post {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

    }
 

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
 
            doTrace()

    }

    /**
     * 清除flag
     */
    fun reset() {
        flag.fill(false)
        doTrace()

    }


    /**
     * 检测是否曝光
     */
    fun doTrace() {

        val layoutManager = recyclerView.layoutManager ?: return
        //获取可见的范围
        val (first, last) = getRange(layoutManager)
        //遍历可见的index 
        for (index in first..last) {
            //如果未曝光过并且在认为曝光的阈值内 调用onShow
            if (index in flag.indices &&
                !flag[index] &&
                boundsCheck(layoutManager.findViewByPosition(index))
            ) {
                flag[index] = true
                onShow(index)
            }
        }

    }

    /**
     * 获取view可见的范围
     * 支持三种LayoutManager的判断
     */
    private fun getRange(layoutManager: RecyclerView.LayoutManager): Pair<Int, Int> {
        var first = -1
        var last = -1
        when (layoutManager) {
            is LinearLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is GridLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is StaggeredGridLayoutManager -> {
                val startPos = IntArray(layoutManager.spanCount)
                val endPos = IntArray(layoutManager.spanCount)
                layoutManager.findFirstVisibleItemPositions(startPos)
                layoutManager.findLastVisibleItemPositions(endPos)
                var start = startPos[0]
                var end = endPos[0]
                for (i in 1 until startPos.size) {
                    if (start > startPos[i]) {
                        start = startPos[i]
                    }
                }
                for (i in 1 until endPos.size) {
                    if (end < endPos[i]) {
                        end = endPos[i]
                    }
                }
                first = start
                last = end
            }
        }
        return first to last
    }

    /**
     * 检查view是否在设置的可见阈值之内
     */
    private fun boundsCheck(view: View?): Boolean {
        if (view == null) return false
        val rect = Rect()

        if (view.getLocalVisibleRect(rect)) {
            val height = view.height.toDouble()
            val width = view.width.toDouble()
            val l = rect.left.toDouble()
            val t = rect.top.toDouble()
            val r = rect.right.toDouble()
            val b = rect.bottom.toDouble()
            val visiblePercent = when {
                l != 0.0 -> (width - l) / width
                r != width -> r / width
                t != 0.0 -> (height - t) / height
                b != height -> b / height
                else -> 1.0
            } * 100
            return visiblePercent >= this.visiblePercent
        }

        return false
    }

    private inner class DataObserver : RecyclerView.AdapterDataObserver() {

        //所有都改变
        override fun onChanged() {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

        //改变规定的range
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
            flag.fill(false, positionStart, positionStart + itemCount)
            doTrace()
        }

        //把form移动到to 移动方法类似冒泡
        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
            if (fromPosition == toPosition) {
                return
            }
            var form = fromPosition
            for (i in IntProgression.fromClosedRange(fromPosition, toPosition, toPosition.compareTo(fromPosition))) {
                val temp = flag[form]
                flag[form] = flag[i]
                flag[i] = temp
                form = i
            }

            doTrace()

        }

        //插入新元素到flag
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            val newFlag = BooleanArray(itemCount + flag.size)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart, newFlag, positionStart + itemCount, flag.size - positionStart)
            flag = newFlag

            doTrace()

        }

        //删除flag中的元素
        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {

            val newFlag = BooleanArray(flag.size - itemCount)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart + itemCount, newFlag, positionStart, flag.size - positionStart - itemCount)
            flag = newFlag

            doTrace()
        }

    }
}
复制代码

这样使用就可以了

ItemShowDetector(recyclerView) { it ->
    //do something
}
复制代码

PM:这快速滑动的时候用户也看不清楚内容啊,我想要快速滚动的时候不上报。

pjqyM

快速滑动(Fling)的时候不进行检测

因为继承了OnScrollListener所以可以重写onScrollStateChanged然后根据recyclerview的状态判断是否需要检测

//增加一个状态标识
private var isDragging = false

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    isDragging = newState == RecyclerView.SCROLL_STATE_DRAGGING
  	//滚动停止后检测一下
    if (newState == RecyclerView.SCROLL_STATE_IDLE) doTrace()
}

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
		//拖动的状态才进行检测
    if (isDragging) {
        doTrace()
    }

}
复制代码

PM:我想要卡片被用户划过到隐藏后,如果用户重新滑回来看到卡片了要重新上报。

pjqyM

支持重复曝光

之前讲过OnChildAttachStateChangeListener可以监听item被隐藏,所以我们可以用这个来监听隐藏 然后改变我们的曝光状态就好了

//在ItemShowDetector初始化init{} 的时候 加上OnChildAttachStateChangeListener
recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
            override fun onChildViewAttachedToWindow(view: View) {
            }

            //监听item被移除的事件
            override fun onChildViewDetachedFromWindow(view: View) {
                    recyclerView.getChildLayoutPosition(view).takeIf { it in flag.indices }?.let {
                      //修改状态为false 代表未曝光
                        flag[it] = false
                    }
            }
        })

复制代码

完整代码

class ItemShowDetector(val recyclerView: RecyclerView, val onShow: (position: Int) -> Unit) : RecyclerView.OnScrollListener() {

    /**
     * 可见百分比 0-100
     */
    var visiblePercent = 50

    /**
     * 是否忽略flipping的曝光
     */
    var ignoreFlipping = true

    /**
     * 隐藏后是否需要重新曝光
     */
    var needReshow = false

    /**
     * 保存曝光的状态
     */
    var flag: BooleanArray = BooleanArray(0)

    private var isDragging = false

    private val adapter: RecyclerView.Adapter<*> =
        if (recyclerView.adapter == null) throw RuntimeException("recyclerview未设置adapter") else recyclerView.adapter!!

    init {
        //监听滚动监听
        recyclerView.addOnScrollListener(this)

        recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
            override fun onChildViewAttachedToWindow(view: View) {

            }

            //监听item被移除的事件
            override fun onChildViewDetachedFromWindow(view: View) {
                if (needReshow) {
                    recyclerView.getChildLayoutPosition(view).takeIf { it in flag.indices }?.let {
                        flag[it] = false
                    }
                }
            }
        })

        //监控adapter的数据变化
        adapter.registerAdapterDataObserver(DataObserver())

        //检测初始化的曝光
        recyclerView.post {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

    }

    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        isDragging = newState == RecyclerView.SCROLL_STATE_DRAGGING
        if (newState == RecyclerView.SCROLL_STATE_IDLE) doTrace()
    }

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

        if (!ignoreFlipping || isDragging) {
            doTrace()
        }

    }

    /**
     * 清除flag
     */
    fun reset() {
        flag.fill(false)
        doTrace()

    }


    /**
     * 检测是否曝光
     */
    fun doTrace() {

        val layoutManager = recyclerView.layoutManager ?: return
        //获取可见的范围
        val (first, last) = getRange(layoutManager)
        //遍历可见的index
        for (index in first..last) {
            //如果未曝光过并且在认为曝光的阈值内 调用onShow
            if (index in flag.indices &&
                !flag[index] &&
                boundsCheck(layoutManager.findViewByPosition(index))
            ) {
                flag[index] = true
                onShow(index)
            }
        }

    }

    /**
     * 获取view可见的范围
     * 支持三种LayoutManager的判断
     */
    private fun getRange(layoutManager: RecyclerView.LayoutManager): Pair<Int, Int> {
        var first = -1
        var last = -1
        when (layoutManager) {
            is LinearLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is GridLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is StaggeredGridLayoutManager -> {
                val startPos = IntArray(layoutManager.spanCount)
                val endPos = IntArray(layoutManager.spanCount)
                layoutManager.findFirstVisibleItemPositions(startPos)
                layoutManager.findLastVisibleItemPositions(endPos)
                var start = startPos[0]
                var end = endPos[0]
                for (i in 1 until startPos.size) {
                    if (start > startPos[i]) {
                        start = startPos[i]
                    }
                }
                for (i in 1 until endPos.size) {
                    if (end < endPos[i]) {
                        end = endPos[i]
                    }
                }
                first = start
                last = end
            }
        }
        return first to last
    }

    /**
     * 检查view是否在设置的可见阈值之内
     */
    private fun boundsCheck(view: View?): Boolean {
        if (view == null) return false
        val rect = Rect()

        if (view.getLocalVisibleRect(rect)) {
            val height = view.height.toDouble()
            val width = view.width.toDouble()
            val l = rect.left.toDouble()
            val t = rect.top.toDouble()
            val r = rect.right.toDouble()
            val b = rect.bottom.toDouble()
            val visiblePercent = when {
                l != 0.0 -> (width - l) / width
                r != width -> r / width
                t != 0.0 -> (height - t) / height
                b != height -> b / height
                else -> 1.0
            } * 100
            return visiblePercent >= this.visiblePercent
        }

        return false
    }

    private inner class DataObserver : RecyclerView.AdapterDataObserver() {

        //所有都改变
        override fun onChanged() {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

        //改变规定的range
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
            flag.fill(false, positionStart, positionStart + itemCount)
            doTrace()
        }

        //把form移动到to 移动方法类似冒泡
        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
            if (fromPosition == toPosition) {
                return
            }
            var form = fromPosition
            for (i in IntProgression.fromClosedRange(fromPosition, toPosition, toPosition.compareTo(fromPosition))) {
                val temp = flag[form]
                flag[form] = flag[i]
                flag[i] = temp
                form = i
            }

            doTrace()

        }

        //插入新元素到flag
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            val newFlag = BooleanArray(itemCount + flag.size)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart, newFlag, positionStart + itemCount, flag.size - positionStart)
            flag = newFlag

            doTrace()

        }

        //删除flag中的元素
        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {

            val newFlag = BooleanArray(flag.size - itemCount)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart + itemCount, newFlag, positionStart, flag.size - positionStart - itemCount)
            flag = newFlag

            doTrace()
        }

    }

}
复制代码

使用

ItemShowDetector(recyclerView) { it ->
    //do something
}.apply {
    //设置曝光阈值50%
    visiblePercent = 50
    //设置忽略快速滚动
    ignoreFlipping = true
    //设置需要重新曝光
    needReshow = true
}
复制代码

最后

注意:因为是用AdapterDataObserver来观察recyclerview数据的变化的 所以请不要使用notifydatachanged()来刷新,这样会清空所有记录的曝光状态

如有更好的想法和建议欢迎留言哈哈。。。

猜你喜欢

转载自juejin.im/post/7034040245947629575