Mobile Development 20 min read

Android RecyclerView Item Exposure Tracking Implementation

This article presents a comprehensive solution for tracking and reporting exposure of RecyclerView items in Android, covering data collection during scrolling, visibility changes across fragment and activity lifecycles, handling data updates, and providing customizable callbacks and exposure criteria, all without requiring developers to manage the collection process manually.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Android RecyclerView Item Exposure Tracking Implementation

The article describes an Android feature that automatically collects exposure data for each item in a RecyclerView and notifies developers when exposure criteria are met, eliminating the need for manual data‑collection logic.

Usage

// Set the percentage of view that must be visible to count as exposure
myAdapter.setOutPercent(0.1f);
// Set the minimum time (ms) an item must be visible to count as exposure
myAdapter.setExposureTime(0);

// Register exposure callbacks
myAdapter.setExposureFunc(items -> {
    // items is a list of ExpItem
that met the exposure conditions
    for (ExpItem
item : items) {
        LogUtil.d("kami", "exposure position = " + item.getPostion() + ": " + item.getData().sourceFeed.content + ",duration = " + (item.getEndTime() - item.getStartTime()));
    }
    return null;
}, item -> {
    // Custom filter, e.g., only expose ads
    return item.getData().isAd();
});

The ExpItem class holds the view, position, associated data, and exposure timestamps:

class ExpItem
{
    var itemView: View? = null
    var postion: Int = 0
    var data: T? = null
    var startTime = 0L
    var endTime = 0L
}

Solution Overview

Scroll exposure – collect items when they become attached to the window and evaluate exposure during onScrolled .

Visibility‑change exposure – handle fragment or activity visibility changes to trigger exposure for currently visible items.

Data‑change exposure – listen to adapter data changes and expose newly visible items.

Scroll Exposure Implementation

override fun onViewAttachedToWindow(holder: VH) {
    val item = ExpItem
()
    item.data = holder.mData
    item.itemView = holder.itemView
    item.postion = holder.mPosition
    collectDatas.add(item)
    super.onViewAttachedToWindow(holder)
    if (innerCheckExposureData(item)) {
        item.startTime = TimeUtil.getCurrentTimeMillis()
    }
}

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    super.onScrolled(recyclerView, dx, dy)
    val it = collectDatas.iterator()
    while (it.hasNext()) {
        val item = it.next()
        if (innerCheckExposureData(item)) {
            if (item.startTime == 0L) {
                item.startTime = TimeUtil.getCurrentTimeMillis()
            }
            if (funcCheck == null) {
                expDatas.add(item)
            } else if (funcCheck!!.invoke(item)) {
                expDatas.add(item)
            }
            it.remove()
        }
    }
}

private fun innerCheckExposureData(item: ExpItem<*>): Boolean {
    val rect = Rect()
    val visible = item.itemView!!.getGlobalVisibleRect(rect)
    if (visible) {
        if (rect.height() >= item.itemView!!.measuredHeight * outPercent) {
            return true
        }
    }
    return false
}

When scrolling stops ( RecyclerView.SCROLL_STATE_IDLE ), the collected items are filtered by exposure duration and reported.

Fragment Visibility Handling

The article proposes a BaseFragment that tracks real visibility using two flags: isVisible (actual visibility) and fakeVisible (system‑provided hint). It overrides onResume , onHiddenChanged , and setUserVisibleHint to compute true visibility and calls onFragmentResume / onFragmentPause accordingly. Child fragments are recursively notified.

@Override
public void onResume() {
    super.onResume();
    if (fromVisibleHint) {
        fakeVisible = getUserVisibleHint();
    } else {
        fakeVisible = !isHidden();
    }
    if (getParentFragment() != null && getParentFragment() instanceof BaseFragment) {
        return;
    }
    if (fakeVisible) {
        isVisible = true;
        onFragmentResume(true);
    }
}

@Override
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    onTabChanged(!hidden);
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    fromVisibleHint = true;
    onTabChanged(isVisibleToUser);
}

Using a LifecycleUtil , the fragment or activity lifecycle events trigger exposureVisible() , which iterates over currently visible view holders and reports exposure.

private fun exposureVisible() {
    val needExpDatas = getVisibleExposureList()
    if (!needExpDatas.isEmpty()) {
        funcExp?.invoke(needExpDatas)
    }
}

private fun getVisibleExposureList(): ArrayList
> {
    val exposureList = arrayListOf
>()
    for (i in 0 until mRecyclerView.childCount) {
        val itemView = mRecyclerView.getChildAt(i) ?: continue
        val vh = mRecyclerView.getChildViewHolder(itemView)
        if (vh is HyBaseViewHolder<*>) {
            val item = ExpItem
()
            item.data = vh.mData as T
            item.itemView = vh.itemView
            item.postion = vh.mPosition
            if (innerCheckExposureData(item)) {
                if (funcCheck == null || funcCheck!!.invoke(item)) {
                    exposureList.add(item)
                }
                collectDatas.remove(item)
            }
        }
    }
    return exposureList
}

Data‑Change Exposure

The adapter registers an AdapterDataObserver to call exposureVisible() on full data refreshes and to evaluate newly inserted items for exposure.

registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
    override fun onChanged() {
        super.onChanged()
        exposureVisible()
    }
    override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
        super.onItemRangeInserted(positionStart, itemCount)
        val needExpDatas = getVisibleExposureList(positionStart, itemCount)
        if (!needExpDatas.isEmpty()) {
            funcExp?.invoke(needExpDatas)
        }
    }
})

Conclusion

The presented approach enables reliable exposure statistics for RecyclerView items across scrolling, fragment visibility changes, and data updates, offering a simple API for developers to register callbacks and custom filters while the library handles all collection and timing logic internally.

androidKotlinRecyclerViewExposureTrackingFragmentLifecycle
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.