在日常的APP開發(fā)中隧熙,經常會遇到列表Item曝光相關的埋點刺下。我們通常是當數(shù)據(jù)對應的UI元素展示在屏幕上時才算作曝光并進行記錄。所以不可避免地在記錄曝光時需要結合屏幕上的列表數(shù)據(jù)變化來進行。
列表數(shù)據(jù)變化一般會由這幾種事件引起:
(1)列表數(shù)據(jù)刷新
(2)列表滑動
(3)軟鍵盤彈出/收起
所以對應的觀察時機為:
1、頁面在前臺時發(fā)生的以上事件
2际邻、頁面切換到前臺時
列表數(shù)據(jù)刷新
通過注冊AdapterDataObserver來感知列表的刷新行為,并通過while-delay防抖芍阎,afterLatestMeasured來確保ui完成了刷新
private var checkJob: Job? = null
private var checkNeedDelay = false
/**
* 列表刷新感知世曾,防抖
*/
private fun addAdapterDataObserver() {
mRecyclerView.adapter?.registerAdapterDataObserver(ListExpoAdapterDataObserver {
resetForScrollPosition()
checkNeedDelay = true
if (checkJob?.isActive != true) {
checkJob = AccountMainScope().launch {
while (checkNeedDelay) {
checkNeedDelay = false
delay(CHECK_DELAY)
}
mRecyclerView.afterLatestMeasured {
checkExpoItem()
}
}
}
})
}
class ListExpoAdapterDataObserver(
private val checkExpoCallBack: (() -> Unit),
) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
dealCallBack()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
dealCallBack()
}
...
列表滑動
在onScrolled時計算出曝光的范圍,在onScrollStateChanged的SCROLL_STATE_IDLE時根據(jù)曝光范圍進行回調谴咸,獲取曝光元素的相關數(shù)據(jù)信息轮听。并清除緩存的曝光記錄
private var rvExpoFirstPositionForScroll = NO_POSITION
private var rvExpoLastPositionForScroll = NO_POSITION
/**
* 列表滑動感知
*/
private fun addOnScrollListener() {
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE ->{
checkExpoItemForScroll()
resetForScrollPosition()
}
else -> {}
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
cacheExpoItemPositionForScroll()
}
})
}
/**
* 處理滑動中的曝光行為
*/
private fun cacheExpoItemPositionForScroll(){
val (rvExpoFirstPosition, rvExpoLastPosition) = mRecyclerView.checkFirstAndLastItemPosition()
rvExpoFirstPositionForScroll = if(rvExpoFirstPositionForScroll == NO_POSITION){
rvExpoFirstPosition
}else{
rvExpoFirstPosition.coerceAtMost(rvExpoFirstPositionForScroll)
}
rvExpoLastPositionForScroll = if (rvExpoLastPositionForScroll == NO_POSITION) {
rvExpoLastPosition
} else {
rvExpoLastPosition.coerceAtLeast(rvExpoLastPositionForScroll)
}
}
軟鍵盤彈出/收起
private fun addKeyBoardListener() {
(mRecyclerView.context as? AppCompatActivity)?.let {
ListExpoKeyboardChangeListener(it, object : ListExpoKeyboardChangeListener.KeyboardHeightListener {
override fun onKeyboardHeightChanged(keyboardHeight: Int, keyboardOpen: Boolean, isLandscape: Boolean) {
checkExpoItem()
}
})
}
}
使用lifecycle進行生命周期的感知肿轨,在頁面回到前臺時檢查當前的item曝光情況,在頁面離開前臺時進行數(shù)據(jù)上報操作
/**
* 頁面生命周期感知
*/
private fun addLifeCycleListener() {
mLifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
checkExpoItem()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
sendEventAndReset()
}
})
}
至此蕊程,我們可以通過在合適的檢查時機使用layoutManager的findFirstVisibleItemPosition,findLastVisibleItemPosition方法驼唱,實現(xiàn)對RecyclerView.Adapter適配器中內容的曝光上報需求藻茂。
但隨著ConcatAdapter的出現(xiàn)(順序組合多個RecyclerView.Adapter,并顯示在一個RecyclerView中)玫恳,我們也需要對其進行適配辨赐。
由于我們只能通過RecyclerView的LayoutManager來獲取可見item的坐標,無法通過RecyclerView.Adapter來獲取京办,所以我們需要通過ConcatAdapter順序組合的特性掀序,根據(jù)RecycleView首末item的顯示位置、多個RecyclerView.Adapter的ItemCount惭婿,來計算出我們關注的Adapter的曝光情況不恭。
/**
* 處理曝光數(shù)據(jù)獲取
*/
private fun dealDataProvided(listener: ListExpoListener<*>, rvExpoFirstPosition: Int, rvExpoLastPosition: Int) {
val expoAdapterItemCount = listener.getExpoAdapterItemCount()
if (rvExpoFirstPosition == NO_POSITION || rvExpoLastPosition == NO_POSITION || expoAdapterItemCount == 0) return
if (mRecyclerView.adapter is ConcatAdapter) {
var expoAdapterFirstIndex = 0
run loop@{
(mRecyclerView.adapter as ConcatAdapter).adapters.forEach { adapter ->
if (adapter == listener.expoAdapter) return@loop
expoAdapterFirstIndex += adapter.itemCount
}
}
val expoAdapterLastIndex = expoAdapterFirstIndex + expoAdapterItemCount - 1
val fromIndex = expoAdapterFirstIndex.coerceAtLeast(rvExpoFirstPosition)
val toIndex = expoAdapterLastIndex.coerceAtMost(rvExpoLastPosition)
if (toIndex >= fromIndex) {
listener.dealDataProvided(fromIndex - expoAdapterFirstIndex, toIndex - expoAdapterFirstIndex)
}
} else {
if (mRecyclerView.adapter == listener.expoAdapter) {
if (rvExpoLastPosition >= rvExpoFirstPosition) {
listener.dealDataProvided(rvExpoFirstPosition, rvExpoLastPosition)
}
}
}
}
至此,我們也實現(xiàn)了對在ConcatAdapter中的RecyclerView.Adapter曝光觀察财饥。
但需求總是變化無常的换吧,如果我們需要對ConcatAdapter中多個RecyclerView.Adapter進行曝光觀察,又應該如何處理呢钥星?
于是我想到了addXXXListener的形式沾瓦,將單個RecyclerView.Adapter的觀察和上報抽成一個ListExpoListener進行封裝∏矗考慮到應對各種不同的數(shù)據(jù)類型以及使用便捷性贯莺,引入了泛型機制
class ListExpoListener<T>(
val expoAdapter: ExpoAdapterInterface,
private val dataProvided: ((expoInfo: ListExpoEntity<T>) -> Unit),
private val reportTrack: ((Map<String, T>) -> Unit) = { },
) {
private val reportDataMap: MutableMap<String, T> by lazy { mutableMapOf() }
fun getExpoAdapterItemCount() = expoAdapter.getItemCountForExpo()
fun dealDataProvided(fromIndex: Int, toIndex: Int) {
dataProvided.invoke(ListExpoEntity(reportDataMap, fromIndex, toIndex))
}
fun dealReportTrack() {
reportTrack.invoke(reportDataMap)
}
fun clearCache(){
reportDataMap.clear()
}
fun getCacheMap() = reportDataMap
/**
* 檢查列表曝光,過濾不可見狀態(tài)的
*/
private fun checkExpoItem() {
if (mLifecycle.currentState == Lifecycle.State.RESUMED && listenerList.isNotEmpty()) {
val (rvExpoFirstPosition, rvExpoLastPosition) = mRecyclerView.checkFirstAndLastItemPosition()
catch {
listenerList.forEach {
dealDataProvided(it, rvExpoFirstPosition, rvExpoLastPosition)
}
}
}
}
/**
* 上報緩存的埋點信息宁改,并重置緩存數(shù)據(jù)
*/
private fun sendEventAndReset() {
listenerList.forEach {
it.dealReportTrack()
it.clearCache()
}
}
至此缕探,可實現(xiàn)目前所接收到的大部分列表埋點需求