PM: I have a request for you. After the card is displayed on the interface, I want you to report the data to me.

I:

A simple approach

// Add a listener
recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
    // Item is added to the view
    override fun onChildViewAttachedToWindow(view: View) {
        recyclerView.getChildLayoutPosition(view)// Get the corresponding position
            .takeIf { it inadapter.currentList.indices }? .let {// Do not cross the boundary to operate
                // Implement the corresponding logic}}// The item is removed to the view
    override fun onChildViewDetachedFromWindow(view: View){}})Copy the code

PM: You just showed a little bit and reported, I want to show 50% before reporting.

I:

Supports methods to show scale Settings

The previous method does not support scaling, but we can add an OnScrollListener to recyclerView and use it to determine the size of the item as the user scrolls


class ItemShowDetector(val recyclerView: RecyclerView, val onShow: (position: Int) - >Unit) : RecyclerView.OnScrollListener() {

    /** * visible percentages 0-100 */
    var visiblePercent = 50
 

    /** * save the exposure state */
    var flag: BooleanArray = BooleanArray(0)


    private val adapter: RecyclerView.Adapter<*> =
        if (recyclerView.adapter == null) throw RuntimeException("Recyclerview not set adapter") else recyclerView.adapter!!

    init {
        // Listen scroll listen
        recyclerView.addOnScrollListener(this)
 

        // Monitor adapter data changes
        adapter.registerAdapterDataObserver(DataObserver())

        // Check the initial exposure
        recyclerView.post {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

    }
 

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
 
            doTrace()

    }

    /** * Clear flag */
    fun reset(a) {
        flag.fill(false)
        doTrace()

    }


    /** * check whether exposure */
    fun doTrace(a) {

        vallayoutManager = recyclerView.layoutManager ? :return
        // Get the visible range
        val (first, last) = getRange(layoutManager)
        // Iterate over the visible index
        for (index infirst.. last) {// Call onShow if it is unexposed and within the perceived exposure threshold
            if (index inflag.indices && ! flag[index] && boundsCheck(layoutManager.findViewByPosition(index)) ) { flag[index] =true
                onShow(index)
            }
        }

    }

    /** * Get the view visible range * support three LayoutManager judgments */
    private fun getRange(layoutManager: RecyclerView.LayoutManager): Pair<Int.Int> {
        var first = -1
        var last = -1
        when (layoutManager) {
            is LinearLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is GridLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is StaggeredGridLayoutManager -> {
                val startPos = IntArray(layoutManager.spanCount)
                val endPos = IntArray(layoutManager.spanCount)
                layoutManager.findFirstVisibleItemPositions(startPos)
                layoutManager.findLastVisibleItemPositions(endPos)
                var start = startPos[0]
                var end = endPos[0]
                for (i in 1 until startPos.size) {
                    if (start > startPos[i]) {
                        start = startPos[i]
                    }
                }
                for (i in 1 until endPos.size) {
                    if (end < endPos[i]) {
                        end = endPos[i]
                    }
                }
                first = start
                last = end
            }
        }
        return first to last
    }

    /** * Check whether the view is within the set visibility threshold */
    private fun boundsCheck(view: View?).: Boolean {
        if (view == null) return false
        val rect = Rect()

        if (view.getLocalVisibleRect(rect)) {
            val height = view.height.toDouble()
            val width = view.width.toDouble()
            val l = rect.left.toDouble()
            val t = rect.top.toDouble()
            val r = rect.right.toDouble()
            val b = rect.bottom.toDouble()
            val visiblePercent = when{ l ! =0.0-> (width - l) / width r ! = width -> r / width t ! =0.0-> (height - t) / height b ! = height -> b / heightelse -> 1.0
            } * 100
            return visiblePercent >= this.visiblePercent
        }

        return false
    }

    private inner class DataObserver : RecyclerView.AdapterDataObserver() {

        // All changes
        override fun onChanged(a) {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

        // Change the specified range
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
            flag.fill(false, positionStart, positionStart + itemCount)
            doTrace()
        }

        // Move the form to to
        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
            if (fromPosition == toPosition) {
                return
            }
            var form = fromPosition
            for (i in IntProgression.fromClosedRange(fromPosition, toPosition, toPosition.compareTo(fromPosition))) {
                val temp = flag[form]
                flag[form] = flag[i]
                flag[i] = temp
                form = i
            }

            doTrace()

        }

        // Insert a new element into flag
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            val newFlag = BooleanArray(itemCount + flag.size)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart, newFlag, positionStart + itemCount, flag.size - positionStart)
            flag = newFlag

            doTrace()

        }

        // Remove the elements in flag
        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {

            val newFlag = BooleanArray(flag.size - itemCount)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart + itemCount, newFlag, positionStart, flag.size - positionStart - itemCount)
            flag = newFlag

            doTrace()
        }

    }
}
Copy the code

That’s how it works

ItemShowDetector(recyclerView) { it ->
    //do something
}
Copy the code

PM: The user can’t see the content clearly when scrolling fast. I want to scroll fast without reporting.

I:

They are not tested when they Fling

Because OnScrollListener is inherited, onScrollStateChanged can be overwritten and tested according to recyclerView state

// Add a status identifier
private var isDragging = false

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    isDragging = newState == RecyclerView.SCROLL_STATE_DRAGGING
  	// Check when scrolling stops
    if (newState == RecyclerView.SCROLL_STATE_IDLE) doTrace()
}

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
		// The state of the drag is checked
    if (isDragging) {
        doTrace()
    }

}
Copy the code

PM: I want the card to be hidden by the user and to be re-reported if the user slides back and sees the card.

I:

Support for repeated exposure

Before about OnChildAttachStateChangeListener can monitor item is hidden, so we can use this to listen to hide Then change the state of our exposure

/ / in ItemShowDetector initialization init {} Add OnChildAttachStateChangeListener
recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
            override fun onChildViewAttachedToWindow(view: View){}// Listen for the event when item is removed
            override fun onChildViewDetachedFromWindow(view: View) {
                    recyclerView.getChildLayoutPosition(view).takeIf { it inflag.indices }? .let {// Change the status to false to unexposed
                        flag[it] = false}}})Copy the code

The complete code

class ItemShowDetector(val recyclerView: RecyclerView, val onShow: (position: Int) - >Unit) : RecyclerView.OnScrollListener() {

    /** * visible percentages 0-100 */
    var visiblePercent = 50

    /** * whether to ignore flipping exposure */
    var ignoreFlipping = true

    /** * Whether to reexpose after hiding */
    var needReshow = false

    /** * save the exposure state */
    var flag: BooleanArray = BooleanArray(0)

    private var isDragging = false

    private val adapter: RecyclerView.Adapter<*> =
        if (recyclerView.adapter == null) throw RuntimeException("Recyclerview not set adapter") else recyclerView.adapter!!

    init {
        // Listen scroll listen
        recyclerView.addOnScrollListener(this)

        recyclerView.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener {
            override fun onChildViewAttachedToWindow(view: View){}// Listen for the event when item is removed
            override fun onChildViewDetachedFromWindow(view: View) {
                if (needReshow) {
                    recyclerView.getChildLayoutPosition(view).takeIf { it inflag.indices }? .let { flag[it] =false}}}})// Monitor adapter data changes
        adapter.registerAdapterDataObserver(DataObserver())

        // Check the initial exposure
        recyclerView.post {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

    }

    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        isDragging = newState == RecyclerView.SCROLL_STATE_DRAGGING
        if (newState == RecyclerView.SCROLL_STATE_IDLE) doTrace()
    }

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

        if(! ignoreFlipping || isDragging) { doTrace() } }/** * Clear flag */
    fun reset(a) {
        flag.fill(false)
        doTrace()

    }


    /** * check whether exposure */
    fun doTrace(a) {

        vallayoutManager = recyclerView.layoutManager ? :return
        // Get the visible range
        val (first, last) = getRange(layoutManager)
        // Iterate over the visible index
        for (index infirst.. last) {// Call onShow if it is unexposed and within the perceived exposure threshold
            if (index inflag.indices && ! flag[index] && boundsCheck(layoutManager.findViewByPosition(index)) ) { flag[index] =true
                onShow(index)
            }
        }

    }

    /** * Get the view visible range * support three LayoutManager judgments */
    private fun getRange(layoutManager: RecyclerView.LayoutManager): Pair<Int.Int> {
        var first = -1
        var last = -1
        when (layoutManager) {
            is LinearLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is GridLayoutManager -> {
                first = layoutManager.findFirstVisibleItemPosition()
                last = layoutManager.findLastVisibleItemPosition()
            }
            is StaggeredGridLayoutManager -> {
                val startPos = IntArray(layoutManager.spanCount)
                val endPos = IntArray(layoutManager.spanCount)
                layoutManager.findFirstVisibleItemPositions(startPos)
                layoutManager.findLastVisibleItemPositions(endPos)
                var start = startPos[0]
                var end = endPos[0]
                for (i in 1 until startPos.size) {
                    if (start > startPos[i]) {
                        start = startPos[i]
                    }
                }
                for (i in 1 until endPos.size) {
                    if (end < endPos[i]) {
                        end = endPos[i]
                    }
                }
                first = start
                last = end
            }
        }
        return first to last
    }

    /** * Check whether the view is within the set visibility threshold */
    private fun boundsCheck(view: View?).: Boolean {
        if (view == null) return false
        val rect = Rect()

        if (view.getLocalVisibleRect(rect)) {
            val height = view.height.toDouble()
            val width = view.width.toDouble()
            val l = rect.left.toDouble()
            val t = rect.top.toDouble()
            val r = rect.right.toDouble()
            val b = rect.bottom.toDouble()
            val visiblePercent = when{ l ! =0.0-> (width - l) / width r ! = width -> r / width t ! =0.0-> (height - t) / height b ! = height -> b / heightelse -> 1.0
            } * 100
            return visiblePercent >= this.visiblePercent
        }

        return false
    }

    private inner class DataObserver : RecyclerView.AdapterDataObserver() {

        // All changes
        override fun onChanged(a) {
            flag = BooleanArray(adapter.itemCount)
            doTrace()
        }

        // Change the specified range
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
            flag.fill(false, positionStart, positionStart + itemCount)
            doTrace()
        }

        // Move the form to to
        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
            if (fromPosition == toPosition) {
                return
            }
            var form = fromPosition
            for (i in IntProgression.fromClosedRange(fromPosition, toPosition, toPosition.compareTo(fromPosition))) {
                val temp = flag[form]
                flag[form] = flag[i]
                flag[i] = temp
                form = i
            }

            doTrace()

        }

        // Insert a new element into flag
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            val newFlag = BooleanArray(itemCount + flag.size)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart, newFlag, positionStart + itemCount, flag.size - positionStart)
            flag = newFlag

            doTrace()

        }

        // Remove the elements in flag
        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {

            val newFlag = BooleanArray(flag.size - itemCount)
            System.arraycopy(flag, 0, newFlag, 0, positionStart)
            System.arraycopy(flag, positionStart + itemCount, newFlag, positionStart, flag.size - positionStart - itemCount)
            flag = newFlag

            doTrace()
        }

    }

}
Copy the code

use

ItemShowDetector(recyclerView) { it ->
    //do something
}.apply {
    // Set the exposure threshold to 50%
    visiblePercent = 50
    // Set to ignore fast scrolling
    ignoreFlipping = true
    // The Settings need to be reexposed
    needReshow = true
}
Copy the code

The last

Note: Do not use notifyDatachanged () to refresh the recyclerView data because the AdapterDataObserver is used to observe changes in recyclerView data. This will clear the exposure status of all records

If you have better ideas and suggestions, please leave a comment haha…