Mobile Development 10 min read

Type‑agnostic RecyclerView Item Visibility Detection for Android

The article explains why reporting item exposure in onBindViewHolder is inaccurate, reviews existing scroll‑listener based solutions and their limitations, and then presents a layout‑manager‑independent Kotlin extension that reliably detects RecyclerView item visibility based on visible area percentage, also covering ViewPager2 page visibility handling.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Type‑agnostic RecyclerView Item Visibility Detection for Android

In many Android business projects, developers report list item exposure inside onBindViewHolder() , which fires before the item is actually visible and couples the tracking logic with the adapter, breaking reuse and the open‑closed principle.

The author first shows a simple but flawed implementation:

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList
) {
    ReportUtil.reportShow("material-item-show", materialId)
}

Then a popular Stack Overflow solution is presented that listens to scroll events, obtains the first and last visible positions from a LinearLayoutManager , and calculates each item's visible height percentage. This approach assumes a linear vertical layout and fails for GridLayoutManager or custom managers.

recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
        val layoutManager = recycler.layoutManager as LinearLayoutManager
        val firstPosition = layoutManager.findFirstVisibleItemPosition()
        val lastPosition = layoutManager.findLastVisibleItemPosition()
        for (pos in firstPosition..lastPosition) {
            val view = layoutManager.findViewByPosition(pos)
            if (view != null) {
                val percentage = getVisibleHeightPercentage(view)
            }
        }
    }
    private fun getVisibleHeightPercentage(view: View): Double {
        val itemRect = Rect()
        val isParentViewEmpty = view.getLocalVisibleRect(itemRect)
        val visibleHeight = itemRect.height().toDouble()
        val height = view.measuredHeight
        val viewVisibleHeightPercentage = visibleHeight / height * 100
        return if (isParentViewEmpty) viewVisibleHeightPercentage else 0.0
    }
})

To avoid layout‑manager‑specific code, the author proposes a generic extension method for RecyclerView that iterates over all child views, compares each child’s visible rectangle with its full size, and triggers a callback when the visible area exceeds a configurable threshold (default 50%). The method also handles attaching/detaching listeners to prevent memory leaks.

fun RecyclerView.addOnItemVisibilityChangeListener(
    percent: Float = 0.5f,
    block: (itemView: View, adapterIndex: Int, isVisible: Boolean) -> Unit
) {
    val scrollListener = object : OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            checkVisibility()
        }
    }
    // ... (visibility checking logic that uses getLocalVisibleRect and area comparison)
    addOnScrollListener(scrollListener)
    addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
        override fun onViewDetachedFromWindow(v: View?) {
            if (v is RecyclerView) {
                v.removeOnScrollListener(scrollListener)
                v.removeOnAttachStateChangeListener(this)
            }
        }
        override fun onViewAttachedToWindow(v: View?) {}
    })
}

This solution works for any LayoutManager because it directly queries the RecyclerView ’s child views. It also reports both visibility and invisibility events, enabling richer upstream handling.

For cases where the list is covered by a fragment (e.g., a bottom tab bar), the author adds a global visibility listener that forces all currently visible items to be reported as invisible when the whole RecyclerView becomes hidden.

onVisibilityChange(viewGroups, false) { view, isVisible ->
    if (!isVisible) {
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val adapterIndex = getChildAdapterPosition(child)
            if (adapterIndex in visibleAdapterIndexs) {
                block(child, adapterIndex, false)
                visibleAdapterIndexs.remove(adapterIndex)
            }
        }
    }
}

The article also extends the idea to ViewPager2 , which internally uses a RecyclerView but does not expose it. A custom extension adds a page‑visibility listener that records the previously selected page and invokes callbacks for visibility changes.

fun ViewPager2.addOnPageVisibilityChangeListener(
    block: (index: Int, isVisible: Boolean) -> Unit
) {
    var lastPage = currentItem
    val listener = object : OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            if (lastPage != position) block(lastPage, false)
            block(position, true)
            lastPage = position
        }
    }
    registerOnPageChangeCallback(listener)
    addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
        override fun onViewDetachedFromWindow(v: View?) {
            if (v is ViewPager2 && ViewCompat.isAttachedToWindow(v)) {
                v.unregisterOnPageChangeCallback(listener)
                v.removeOnAttachStateChangeListener(this)
            }
        }
        override fun onViewAttachedToWindow(v: View?) {}
    })
}

By abstracting visibility detection into reusable, layout‑manager‑agnostic extensions, the approach simplifies tracking logic, improves code reuse across different list configurations, and adheres to clean architecture principles.

UIandroidMobileDevelopmentKotlinRecyclerViewExtensionFunctionsVisibilityDetection
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.