Android recyclerView exposure statistics

Keywords: Android

1, Background

In product requirements, we often count the exposure requirements of each item of recyclerView:
The recyclerView scrolls up and down each item from invisible to the visible range of the screen (this includes the visible range of the item and the exposure duration of the item)
During tab switching or page switching, it will cause invisible changes in recyclerView (the item s visible on the current screen are considered as an exposure)
Exposure due to data changes
In order to meet the product requirements, we first need to collect data, collect all item s to be reported in the sliding process, and then report at an appropriate time, such as sliding stop and page switching.

2, Demand analysis

Through the above demand analysis, we can know that the exposure of recyclerView is mainly divided into sliding exposure, visibility change exposure and data change exposure.
1. Sliding exposure
We can monitor the sliding process of recyclerView, collect the exposure data during the sliding process (because the exposure behavior is generated during the sliding process), and then report the exposure when the sliding stops (this can not only ensure real-time performance, but also take into account the performance of the mobile phone).

2. Visibility change exposure
Here, we need to monitor the visibility change of recyclerView, but we are not provided with the monitoring of View visibility change. Although there are some monitoring for focus changes, it does not completely cover the visibility changes of View. Therefore, we must find other ways to achieve this. Here, I think about whether it is possible to realize the onFragmentResume and onFragmentPause of Fragment through onResume and onPause in the life cycle of Actvity to monitor the visibility of Fragment. Monitoring the visibility of Fragment is equivalent to monitoring the visibility of recyclerView. Then traverse the currently visible Item collection and submit it.

3. Exposure due to data changes
Sometimes, data changes will also cause corresponding exposure. This is easier to deal with. We only need to monitor the corresponding data changes. Then expose the visible item s to be exposed.

3, Realize the exposure caused by the sliding process of recyclerView

The implementation principle flow chart is as follows:

Because the Adapter controls the creation and binding of the ViewHolder of the RecyclerView, and the corresponding data adaptation is completed in the Adapter, you choose to rewrite the Adapter to realize the exposure function.

First, we need to collect data during the sliding process of recyclerView, that is, collect the item s to be exposed on the screen.
We know that onViewAttachedToWindow(holder: VH) will be called first when the ViewHolder of the recelerview is loaded on the screen, so we choose this method for data collection. As long as the data displayed on the screen is collected into the collectdata list

 /**
 * Collect exposure items
 */
override fun onViewAttachedToWindow(holder: VH) {
    val item = ExpItem<T>()
    item.data = holder.mData
    item.itemView = holder.itemView
    item.postion = holder.mPosition
    collectDatas.add(item)
    super.onViewAttachedToWindow(holder)
    //Check the exposure range and update the exposure start time
    if (innerCheckExposureData(item)){
        item.startTime = TimeUtil.getCurrentTimeMillis()
    }

}

Then we need to filter the data to be exposed, calculate the position of each Item on the screen, and customize the filter conditions (for example, only expose advertisements). This filter needs to be calculated during the scrolling process of recyclerView, because the exposure range of ViewHolder changes constantly during the scrolling process, Then we move the filtered data from the collectdata to the expdata list

Why should this filtering process be placed in the onscroled process?
First, the exposure behavior is achieved in the sliding process. For example, we constantly slide the recyclerView up and down, resulting in an item_1 constantly appears and disappears on the screen. If this process item_ After 5 exposures, the slide stops and returns to our initial slide position. If we filter the exposed items when the slide stops, we may think that there are no newly exposed items at all. Because our sliding stops at the original initial position, obviously this calculation is wrong. To correctly record exposed items, you must filter the items that meet the exposure conditions during the sliding process of recyclerView.
Secondly, considering the exposure time, when the Item reaches the exposure condition during the sliding process, we should record the start time of exposure. The exposure time cannot be recorded correctly at other times.

Will filtering calculation during onscroled affect the performance of recyclerView and cause unsmooth sliding?
This filter calculation is divided into two parts. One part is the filter logic that needs to be defined by the developer. Here, the developer should pay attention not to have time-consuming judgment logic. The second part is the internal screening logic, which mainly judges whether the exposure height of item meets the exposure requirements.
Will this judgment affect the performance of recyclerView? In fact, it won't.
Firstly, the height and position (sliding offset) of the item itself have been calculated by recyclerView before rendering each frame during the sliding process. Otherwise, recyclerView cannot draw each item in the correct position. So obviously, this calculation will not affect the fluency of recyclerView.
The main judgment we need to make is to get the location information of the current item and compare it to see whether it meets the exposure requirements. Obviously, this is not a time-consuming operation. We are through ItemView Getglobalvisiblerect (rect) is used to obtain the location information of item. Through code tracking, we can see the specific implementation logic

public boolean getChildVisibleRect(
View child, Rect r, android.graphics.Point offset, boolean forceParentCheck) {
        ...
        rect.set(r);
        final int dx = child.mLeft - mScrollX;
        final int dy = child.mTop - mScrollY;
        ...
        rect.offset(dx, dy);
  ...
  rectIsVisible = rect.intersect(0, 0, width, height);
  ...
  return rectIsVisible;
}

public boolean intersect(float left, float top, float right, float bottom) {
   if (this.left < right && left < this.right
            && this.top < bottom && top < this.bottom) {
        if (this.left < left) {
            this.left = left;
        }
        if (this.top < top) {
            this.top = top;
        }
        if (this.right > right) {
            this.right = right;
        }
        if (this.bottom > bottom) {
            this.bottom = bottom;
        }
        return true;
    }
    return false;
}

It can be seen from the code that we obtain the visible range of itemView mainly through the intersection of the current position rect of itemView and the range size of its ViewGroup (i.e. recyclerView). It can be seen from the code that there are some relatively large logic and no time-consuming operations, so there is no need to worry that this will cause the sliding jam of recyclerView.
Then let's take a look at the specific implementation of the screening process

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()
        //Judge exposure range
        if (innerCheckExposureData(item)) {
            if (item.startTime == 0L) {
                item.startTime = TimeUtil.getCurrentTimeMillis()
            }
            if (funcCheck == null) {
              expDatas.add(item)
              //Custom filter criteria
            } else if (funcCheck!!.invoke(item)) {
                expDatas.add(item)
            }
            it.remove()
        }
    }
}

Lines 8-10 of onscroled method are to update the exposure start time, and line 7 of onscroled method is to detect the exposure range

/**
 * Internal judgment: exposure is required only when the visibility of itemView reaches
 */
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
}

Lines 11-16 of onscroled method are our user-defined filter criteria judgment, that is, the filter criteria corresponding to line 10 below

/**
  * Set exposure monitoring
  */
myAdapter.setExposureFunc(items->{
    //Returns the list of data to be exposed
    for (ExpItem<NewFeedBean> item : items) {
        LogUtil.d("kami","exposure position = " + item.getPostion() + ": " +item.getData().sourceFeed.content + ",duration = " + (item.getEndTime() - item.getStartTime()));
    }
    return null;
},item->{
    //Exposure filtering is required for customization: for example, only advertising data is exposed
    return item.getData().isAd();
});

Finally, filter the exposure duration before sliding to stop the exposure data callback. Select the data reaching the exposure duration from expdata, and finally report the data

//Set exposure monitoring
when (newState) {
 //Sliding completion
 RecyclerView.SCROLL_STATE_IDLE ->
    val needExpDatas = getExposureList(expDatas)
     if (!needExpDatas.isEmpty()) {
         funcExp?.invoke(needExpDatas)
     }
 else -> {
 }

Line 5 is our screening of exposure duration

/**
 * Internal judge how long the itemView needs to be exposed when it is exposed
 */
private fun getExposureList(expDatas: ArrayList<ExpItem<T>>): ArrayList<ExpItem<T>> {
    val needExpDatas = ArrayList<ExpItem<T>>()
    val it = expDatas.iterator()
    while (it.hasNext()) {
        val item = it.next()
        if (item.endTime != 0L) {
            if (item.endTime - item.startTime >= exposureTime) {
                needExpDatas.add(item)
            }
            it.remove()
        } else {
            if (TimeUtil.getCurrentTimeMillis() - item.startTime >= exposureTime) {
                item.endTime = TimeUtil.getCurrentTimeMillis()
                needExpDatas.add(item)
                it.remove()
            }
        }
    }
    return needExpDatas
}

When ViewHolder AttachToWindow slides with RecyclerView, we will update the exposure start time. When viewholder detachtowindw and RecyclerView slide stop, we will update the exposure end time.
item.endTime is not zero. If the exposure duration is reached, it means that exposure needs to be added to the exposure list, otherwise it will be discarded.
If the item.endTime is zero, it means that the viewholder is still exposed continuously. The current time is used to calculate and add the exposure time to the exposure list. Otherwise, it will not be processed (because it is still exposed continuously, and the exposure will be carried out when the next sliding reaches the exposure time).
The following is the specific code of onViewDetachedFromWindow, including removing data without exposure and updating the exposure end time.

/**
 * Modify the exposure duration of the removed itemView
 */
override fun onViewDetachedFromWindow(holder: VH) {
    //When Dettached, the data not moved to the expdata list proves that the exposure conditions are not met and no exposure is required. You can remove them from the collectdata list
    val it = collectDatas.iterator()
    while (it.hasNext()) {
        val item = it.next()
        if (holder .mPosition == item.postion && holder.itemView === item.itemView) {
            it.remove()
        }
    }
    //Update exposure end time
    for (expItem in expDatas) {
            if (holder.mPosition == expItem.postion && expItem.itemView === holder.itemView) {
                expItem.endTime = TimeUtil.getCurrentTimeMillis()

            }
    }
    super.onViewDetachedFromWindow(holder)
}

In this way, we have completed the reporting caused by the RecyclerView during the sliding process.

Posted by project168 on Sun, 17 Oct 2021 18:38:30 -0700