recyclerView中的item曝光逻辑实现

在日常的APP开发中,经常会遇到列表Item曝光相关的埋点。我们通常是当数据对应的UI元素展示在屏幕上时才算作曝光并进行记录。所以不可避免地在记录曝光时需要结合屏幕上的列表数据变化来进行。

列表数据变化一般会由这几种事件引起:
(1)列表数据刷新
(2)列表滑动
(3)软键盘弹出/收起

所以对应的观察时机为:
1、页面在前台时发生的以上事件
2、页面切换到前台时

列表数据刷新

通过注册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时根据曝光范围进行回调,获取曝光元素的相关数据信息。并清除缓存的曝光记录


  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曝光情况,在页面离开前台时进行数据上报操作

  /**
   * 页面生命周期感知
   */
  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方法,实现对RecyclerView.Adapter适配器中内容的曝光上报需求。

但随着ConcatAdapter的出现(顺序组合多个RecyclerView.Adapter,并显示在一个RecyclerView中),我们也需要对其进行适配。

由于我们只能通过RecyclerView的LayoutManager来获取可见item的坐标,无法通过RecyclerView.Adapter来获取,所以我们需要通过ConcatAdapter顺序组合的特性,根据RecycleView首末item的显示位置、多个RecyclerView.Adapter的ItemCount,来计算出我们关注的Adapter的曝光情况。

  /**
   * 处理曝光数据获取
   */
  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)
        }
      }
    }
  }

至此,我们也实现了对在ConcatAdapter中的RecyclerView.Adapter曝光观察。
但需求总是变化无常的,如果我们需要对ConcatAdapter中多个RecyclerView.Adapter进行曝光观察,又应该如何处理呢?

于是我想到了addXXXListener的形式,将单个RecyclerView.Adapter的观察和上报抽成一个ListExpoListener进行封装。考虑到应对各种不同的数据类型以及使用便捷性,引入了泛型机制

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



/**
   * 检查列表曝光,过滤不可见状态的
   */
  private fun checkExpoItem() {
    if (mLifecycle.currentState == Lifecycle.State.RESUMED && listenerList.isNotEmpty()) {
      val (rvExpoFirstPosition, rvExpoLastPosition) = mRecyclerView.checkFirstAndLastItemPosition()
      catch {
        listenerList.forEach {
          dealDataProvided(it, rvExpoFirstPosition, rvExpoLastPosition)
        }
      }
    }
  }

  /**
   * 上报缓存的埋点信息,并重置缓存数据
   */
  private fun sendEventAndReset() {
    listenerList.forEach {
      it.dealReportTrack()
      it.clearCache()
    }
  }

至此,可实现目前所接收到的大部分列表埋点需求

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容