PM:我给你提个需求,卡片展示在界面上后,我要你上报数据给我。
我:
简单做法
//添加监听
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%才上报。
我:
支持展示比例设置的方法
之前那种方法的回调不支持展示比例的修改,但是我们可以给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:这快速滑动的时候用户也看不清楚内容啊,我想要快速滚动的时候不上报。
我:
快速滑动(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:我想要卡片被用户划过到隐藏后,如果用户重新滑回来看到卡片了要重新上报。
我:
支持重复曝光
之前讲过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()
来刷新,这样会清空所有记录的曝光状态
如有更好的想法和建议欢迎留言哈哈。。。