背景
项目中首页列表页需要统计每个item的曝光情况,给产品运营提供数据报表分析用户行为,于是封装了一个通用的列表Item曝光工具,方便曝光埋点上报
源码分析
- 核心就是监听RecyclerView的滚动,在滚动状态为SCROLL_STATE_IDLE的时候开始计算哪些item是可见的
private fun calculateVisibleItemInternal() {
if (!isRecording) {
return
}
val lastRange = currVisibleRange
val currRange = findItemVisibleRange()
val newVisibleItemPosList = createCurVisiblePosList(lastRange, currRange)
visibleItemCheckTasks.forEach {
it.updateVisibleRange(currRange)
}
if (newVisibleItemPosList.isNotEmpty()) {
VisibleCheckTimerTask(newVisibleItemPosList, this, threshold).also {
visibleItemCheckTasks.add(it)
}.execute()
}
currVisibleRange = currRange
}
- 根据LayoutManager找出当前可见item的范围,剔除掉显示不到80%的item
private fun findItemVisibleRange(): IntRange {
return when (val lm = currRecyclerView?.layoutManager) {
is GridLayoutManager -> {
val first = lm.findFirstVisibleItemPosition()
val last = lm.findLastVisibleItemPosition()
return fixCurRealVisibleRange(first, last)
}
is LinearLayoutManager -> {
val first = lm.findFirstVisibleItemPosition()
val last = lm.findLastVisibleItemPosition()
return fixCurRealVisibleRange(first, last)
}
is StaggeredGridLayoutManager -> {
val firstItems = IntArray(lm.spanCount)
lm.findFirstVisibleItemPositions(firstItems)
val lastItems = IntArray(lm.spanCount)
lm.findLastVisibleItemPositions(lastItems)
val first = when (RecyclerView.NO_POSITION) {
firstItems[0] -> {
firstItems[lm.spanCount - 1]
}
firstItems[lm.spanCount - 1] -> {
firstItems[0]
}
else -> {
min(firstItems[0], firstItems[lm.spanCount - 1])
}
}
val last = when (RecyclerView.NO_POSITION) {
lastItems[0] -> {
lastItems[lm.spanCount - 1]
}
lastItems[lm.spanCount - 1] -> {
lastItems[0]
}
else -> {
max(lastItems[0], lastItems[lm.spanCount - 1])
}
}
return fixCurRealVisibleRange(first, last)
}
else -> {
IntRange.EMPTY
}
}
}
- 对可见的item进行分组,形成一个位置列表,上一次和这一次的分开组队
private fun createCurVisiblePosList(lastRange: IntRange, currRange: IntRange): List<Int> {
val result = mutableListOf<Int>()
currRange.forEach { pos ->
if (pos !in lastRange) {
result.add(pos)
}
}
return result
}
- 将分组好的列表装进一个定时的task,在延迟一个阈值的时间后执行onVisibleCheck
class VisibleCheckTimerTask(
input: List<Int>,
private val callback: VisibleCheckCallback,
private val delay: Long
) : Runnable {
private val visibleList = mutableListOf<Int>()
private var isExecuted = false
init {
visibleList.addAll(input)
}
fun updateVisibleRange(keyRange: IntRange) {
val iterator = visibleList.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (entry !in keyRange) {
iterator.remove()
}
}
}
override fun run() {
callback.onVisibleCheck( this, visibleList)
}
fun execute() {
if (isExecuted) {
return
}
mHandler.postDelayed(this, delay)
isExecuted = true
}
fun cancel() {
mHandler.removeCallbacks(this)
}
interface VisibleCheckCallback {
fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>)
}
}
- 达到阈值后,再获取一遍可见item的范围,对于仍然可见的item回调onItemShow
override fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>) {
val visibleRange = findItemVisibleRange()
visibleList.forEach {
if (it in visibleRange) {
notifyItemShow(it)
}
}
visibleItemCheckTasks.remove(task)
}
完整源码
val mHandler = Handler(Looper.getMainLooper())
class ListItemExposeUtil(private val threshold: Long = 100)
: RecyclerView.OnScrollListener(), VisibleCheckTimerTask.VisibleCheckCallback {
private var currRecyclerView: RecyclerView? = null
private var isRecording = false
private var currVisibleRange: IntRange = IntRange.EMPTY
private val visibleItemCheckTasks = mutableListOf<VisibleCheckTimerTask>()
private val itemShowListeners = mutableListOf<OnItemShowListener>()
fun attachTo(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(this)
currRecyclerView = recyclerView
}
fun start() {
isRecording = true
currRecyclerView?.post {
calculateVisibleItemInternal()
}
}
fun stop() {
visibleItemCheckTasks.forEach {
it.cancel()
}
visibleItemCheckTasks.clear()
currVisibleRange = IntRange.EMPTY
isRecording = false
}
fun detach() {
if (isRecording) {
stop()
}
itemShowListeners.clear()
currRecyclerView?.removeOnScrollListener(this)
currRecyclerView = null
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
calculateVisibleItemInternal()
}
}
private fun calculateVisibleItemInternal() {
if (!isRecording) {
return
}
val lastRange = currVisibleRange
val currRange = findItemVisibleRange()
val newVisibleItemPosList = createCurVisiblePosList(lastRange, currRange)
visibleItemCheckTasks.forEach {
it.updateVisibleRange(currRange)
}
if (newVisibleItemPosList.isNotEmpty()) {
VisibleCheckTimerTask(newVisibleItemPosList, this, threshold).also {
visibleItemCheckTasks.add(it)
}.execute()
}
currVisibleRange = currRange
}
private fun findItemVisibleRange(): IntRange {
return when (val lm = currRecyclerView?.layoutManager) {
is GridLayoutManager -> {
val first = lm.findFirstVisibleItemPosition()
val last = lm.findLastVisibleItemPosition()
return fixCurRealVisibleRange(first, last)
}
is LinearLayoutManager -> {
val first = lm.findFirstVisibleItemPosition()
val last = lm.findLastVisibleItemPosition()
return fixCurRealVisibleRange(first, last)
}
is StaggeredGridLayoutManager -> {
val firstItems = IntArray(lm.spanCount)
lm.findFirstVisibleItemPositions(firstItems)
val lastItems = IntArray(lm.spanCount)
lm.findLastVisibleItemPositions(lastItems)
val first = when (RecyclerView.NO_POSITION) {
firstItems[0] -> {
firstItems[lm.spanCount - 1]
}
firstItems[lm.spanCount - 1] -> {
firstItems[0]
}
else -> {
min(firstItems[0], firstItems[lm.spanCount - 1])
}
}
val last = when (RecyclerView.NO_POSITION) {
lastItems[0] -> {
lastItems[lm.spanCount - 1]
}
lastItems[lm.spanCount - 1] -> {
lastItems[0]
}
else -> {
max(lastItems[0], lastItems[lm.spanCount - 1])
}
}
return fixCurRealVisibleRange(first, last)
}
else -> {
IntRange.EMPTY
}
}
}
/**
* 检查该item是否真实可见
* view区域80%显示出来就算
*/
private fun checkItemCurrRealVisible(pos: Int): Boolean {
val holder = currRecyclerView?.findViewHolderForAdapterPosition(pos)
return if (holder == null) {
false
} else {
val rect = Rect()
holder.itemView.getGlobalVisibleRect(rect)
if (holder.itemView.width > 0 && holder.itemView.height > 0) {
return (rect.width() * rect.height() / (holder.itemView.width * holder.itemView.height).toFloat()) > 0.8f
} else {
return false
}
}
}
/**
* 双指针寻找真实可见的item的范围(有一些item没显示完整剔除)
*/
private fun fixCurRealVisibleRange(first: Int, last: Int): IntRange {
return if (first >= 0 && last >= 0) {
var realFirst = first
while (!checkItemCurrRealVisible(realFirst) && realFirst <= last) {
realFirst++
}
var realLast = last
while (!checkItemCurrRealVisible(realLast) && realLast >= realFirst) {
realLast--
}
if (realFirst <= realLast) {
realFirst..realLast
} else {
IntRange.EMPTY
}
} else {
IntRange.EMPTY
}
}
/**
* 创建当前可见的item位置列表
*/
private fun createCurVisiblePosList(lastRange: IntRange, currRange: IntRange): List<Int> {
val result = mutableListOf<Int>()
currRange.forEach { pos ->
if (pos !in lastRange) {
result.add(pos)
}
}
return result
}
/**
* 达到阈值后,再获取一遍可见item的范围,对于仍然可见的item回调onItemShow
*/
override fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>) {
val visibleRange = findItemVisibleRange()
visibleList.forEach {
if (it in visibleRange) {
notifyItemShow(it)
}
}
visibleItemCheckTasks.remove(task)
}
private fun notifyItemShow(pos: Int) {
itemShowListeners.forEach {
it.onItemShow(pos)
}
}
fun addItemShowListener(listener: OnItemShowListener) {
itemShowListeners.add(listener)
}
}
interface OnItemShowListener {
fun onItemShow(pos: Int)
}
class VisibleCheckTimerTask(
input: List<Int>,
private val callback: VisibleCheckCallback,
private val delay: Long) : Runnable {
private val visibleList = mutableListOf<Int>()
private var isExecuted = false
init {
visibleList.addAll(input)
}
fun updateVisibleRange(keyRange: IntRange) {
val iterator = visibleList.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (entry !in keyRange) {
iterator.remove()
}
}
}
override fun run() {
callback.onVisibleCheck(this, visibleList)
}
fun execute() {
if (isExecuted) {
return
}
mHandler.postDelayed(this, delay)
isExecuted = true
}
fun cancel() {
mHandler.removeCallbacks(this)
}
interface VisibleCheckCallback {
fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>)
}
}
- 测试代码
- 运行结果