业务背景
RecyclerView ItemView满足曝光限定,则立刻将相关信息上报服务器,作为后续产品设计、数据下发的依据。
实现思路
- 自定义RecyclerView监听滚动事件,判断所有ItemView曝光面积是否满足限定,满足则加入曝光管理器的待上报队列,否则将其从队列中移除,为什么需要移除?在RecyclerView快速滚动的时候,ItemView快速显示隐藏,曝光时间可能无法满足限定,所以需要移除。
- 考虑到曝光面积、时间满足限定则立马上报,而不是依赖ItemView滑走、Activity/Fragment关闭等触发,所以曝光管理器采用Handler发送延时消息的方案;
曝光条件
- View曝光面积需大于等于自身面积的2/3,可使用getGlobalVisibleRect解决;
- 曝光时间需大于等于1500毫秒,可用Handler().sendMessageDelayed解决;
- 同一条数据一个Activity生命周期只上报一次,可维护上传集合去重来解决;
流程图
根据以上分析,需设计有自定义RecyclerView以及曝光管理器ExposeManager。
- 自定义RecyclerView 自定义RecyclerView,其负责监听ItemView的曝光面积。
class ExposeRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : RecyclerView(//自定义RecyclerView
context, attrs, defStyle
) {
interface ItemViewVisibleListener {//回调接口(显示、隐藏)
fun onItemViewVisible(position: Int)
fun onItemViewGone(position: Int)
}
// 回调监听器
var itemViewVisibleListener: ItemViewVisibleListener? = null
init {
//注册滚动监听
addOnScrollListener(object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
handleScrolled()
}
})
}
//处理滚动回调,第一次加载数据也会触发
private fun handleScrolled() {
when (layoutManager) {
is LinearLayoutManager -> {
handleVisibleItem(
recyclerView,
layoutManager.findFirstVisibleItemPosition(),
layoutManager.findLastVisibleItemPosition(),
layoutManager.itemCount
)
}
}
}
private fun handleVisibleItem(
recyclerView: RecyclerView, firstVisiblePosition: Int, lastVisiblePosition: Int, itemCount: Int
) {
for (i in 0 until itemCount) {//1
when (i) {
in firstVisiblePosition..lastVisiblePosition -> {
callListenerByItemViewRect(recyclerView.findViewHolderForAdapterPosition(i))
}
else -> {
itemViewVisibleListener?.onItemViewGone(i)
}
}
}
}
//回调不同的接口
private fun callListenerByItemViewRect(viewHolder: ViewHolder?) {
viewHolder?.apply {
if (itemView.isExpose()) {
itemViewVisibleListener?.onItemViewVisible(adapterPosition)
} else {
itemViewVisibleListener?.onItemViewGone(adapterPosition)
}
}
}
//判断曝光面积是否满足条件
fun View.isExpose(): Boolean {//2
val rect = Rect()
val isVisibleRect: Boolean = this.getGlobalVisibleRect(rect)
return (isVisibleRect
&& rect.height() * rect.width() >= measuredHeight * measuredWidth * 2 / 3)
}
}
重点分析以下2个点,其他比较简单,看看注释就差不多了。
- 注释1:在[firstVisiblePosition,lastVisiblePosition]区域则认为可见,需进一步判断曝光面积,否则回调不可见;为什么需要回调不可见呢?上面有解释,大家可以返回看下;
- 注释2:getGlobalVisibleRect获取当前ItemView可视区域,返回true则为可见,且rect记录可见区域大小,条件rect.height() * rect.width() >= measuredHeight * measuredWidth * 2 / 3,判断可见区域是否大于等于View面积的2/3;
曝光管理器ExposeManager
负责处理延时的曝光任务。利用Handler自带队列的特性,将满足曝光面积满足限定的添加到队列,延时处理。
class ExposeManager {
companion object {
const val VISIBLE_TIME = 1500L//曝光时间
}
private val uploadedEntityList = ArrayList<Entity>()//记录已上报的,避免重复上报
private val handlerThread = HandlerThread("expose_thread")//曝光处理线程,防止堵塞主线程
private val handler: Handler
init {
handlerThread.start()
handler = object : Handler(handlerThread.looper) {
override fun handleMessage(msg: Message) {//曝光时间到,触发上报
super.handleMessage(msg)
uploadedEntityList.add(msg.obj as Entity)
uploadEntity()
}
}
}
//判断是否上报过
private fun isAlreadyUpload(entity: Entity): Boolean {
return uploadedEntityList.any {
entity.id.hashCode() == it.id.hashCode()
}
}
//对于隐藏时触发
fun addEntity(entity: Entity) {//1
if (handler.hasMessages(entity.id.hashCode())) {
return
}
if (isAlreadyUpload(entity)) {
return
}
handler.sendMessageDelayed(Message.obtain().apply {
this.what = entity.id.hashCode()
this.obj = entity
}, VISIBLE_TIME)
}
//对于view隐藏时触发
fun removeEntity(entity: Entity) {//2
handler.removeMessages(entity.hashCode())
}
//触发上报
private fun uploadEntity() {}
//销毁线程,不满足曝光时间的实体,将无法触发上报
fun destroy() {
handlerThread.quit()
}
//曝光实体
data class Entity(val id: Long)
}
重点分析以下2个点,其他比较简单,看看注释就差不多了。
- 注释1:当ItemView曝光面积满足限定时触发,如果Handler队列中没有且没有上报过,则发送延时任务,曝光时间达到则上报;
- 注释2:当ItemView曝光面积不满足限定时触发,如果Handler队列没有相同的任务,则移除失败,内部会自动处理。
如何使用
resourceRv.itemViewVisibleListener = object :
ExposeRecyclerView.ItemViewVisibleListener {
override fun onItemViewVisible(position: Int) {
//调用ExposeManager addEntity()添加
}
override fun onItemViewGone(position: Int) {
//调用ExposeManager removeEntity()移除
}
}
总结
- 分层设计,自定义RecyclerView负责监听ItemView的曝光面积;而ExposeManager负责处理曝光时间、待上报任务队列、上报成功任务队列,两者不应该耦合在一起,责任单一,也好维护;
- 曝光发生会比较频繁,如果是在主线程处理任务,可能会堵塞主线程,所以考虑使用HandlerThread;
- 使用handlerThread.quit()销毁线程,不满足曝光时间限定的任务,将无法触发上报,使用quitSafely效果也是一样,因为这里都是延迟消息;
以上分析有不对的地方,请指出,互相学习,谢谢哦!