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…