Author: Chen Heqiang
The introduction
❝
When an Internet product reaches a certain level, in order to further improve user experience, most products will make statistics on the exposure times and duration of the displayed content. Based on this, the reading habits and behavior paths of users can be inferred, and the product can be optimized after analysis.
Currently, buried point collection methods of Android clients can be divided into three categories:
Code buried point
Directly upload the buried data to the location where the buried data is needed. Advantages are high accuracy and flexible access to service data parameters. The disadvantage is that the code workload is large, intrusive, complex maintenance and so on.
Visual burial point
Configure the data to be collected using the visual tool, configure the buried point information on the background, and upload the buried point data on the client based on the background configuration. The advantage is that the burying point is relatively simple, the client does not rely on development, and the flexibility is strong. The disadvantages are high upfront development costs and the inability to capture complex business parameters.
No code burying point
It is not that the code burying point is not really needed, but the response events of the system are intercepted by hook on the client side, all events are automatically collected and the burying point data is reported, and useful data is filtered out at the back end. Advantage is comprehensive coverage, disadvantage is large amount of data useless data.
Buried point collection events on the client can be divided into three categories
-
Browsing page events
-
Element click event
-
Content exposure Event
Page browsing and element click events are relatively simple, only need to trigger buried point acquisition events in the corresponding scene. Content exposure events are more complex, because the trigger time of content exposure depends not only on the state of the page, but also on the dynamic location of the content and the life cycle of the parent component. So content exposure events basically adopt manual code burying points. This article mainly analyzes the scenes of content exposure events.
The business situation
Snowball App home page information flow is achieved through RecyclerView, exposure statistics code is as follows:
Class TimelineLogger {companion object {const val STATUS_DISPLAY_DURATION = 2000 Value is timestamp private var startShowTime = LongSparseArray<Long>() /** * Get the currently visible data set */ private fun GetVisibleStatusArray ():LongSparseArray<Long>{val visibleStatusArray = LongSparseArray<Long>() // define the visible information collection val TimeCurrent = system.currentTimemillis () var realFirstPosition = LinearLayoutManager. FindFirstVisibleItemPosition () / / list is the last data val realLastPosition = linearLayoutManager.findLastVisibleItemPosition() for (i in realFirstPosition.. RealLastPosition) {// Traverse the visible data adapter.getitem (I)? .let { status -> if (! StartShowTime. Either containsKey (status. StatusId)) {/ / record visible data start time startShowTime. Put (status. StatusId, TimeCurrent)} visibleStatusArray.put(statusId, statusId, TimeCurrent)}}} /** * immediately: timeCurrent (immediately:Boolean){val visibleStatus = Val timeCurrent = system.currentTimemillis () // For (I in 0 until Startshowtime.size ()) {val key = startShowtime.keyat (I) // Immediately set to false will expose all posts that are no longer displayed / / immediately to true, on behalf of the page is closed to all posts to exposure the if (immediately = = visibleStatus. Either containsKey (key)) {statusTime. Get (key)? .let {timeBegin -> val duration = timeCurrent - timeBegin // Exposure duration if (duration > STATUS_DISPLAY_DURATION) {// Trigger exposure} /** * HomeFragment: fragment {/** * initialize recyclerView */ fun initRecyclerView(){ recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {super.onscrolled (recyclerView, dx, dy) // TimelineLogger. LogStatus (false)}} /** * Current page display */ Override fun onPageSelected() { Timelinelogger.logstatus (false) /** * The current page is not displayed */ Override fun onPageUnselected() {override fun onPageUnselected() { Timelinelogger. logStatus(true) // Leaving the page triggers exposure statistics logic}}Copy the code
The above is a very common exposure statistics code in Android clients, the logic is as follows:
-
When the RecyclerView is visible, the current display information is saved to the data set
-
As the component slides, the information that moves out of the screen is exposed, and the information that moves into the screen is saved to the data set
-
Expose all information in the data set when the page status becomes invisible
There are two drawbacks to writing this way
-
Poor reusability:
Each component has its own unique lifecycle callback method, making it nearly impossible to reuse code between different components. For example, the Feed stream in the figure above generally uses RecyclerView component, while the Banner bit uses ViewPager component. Exposure triggering is dependent on the upper container component and is not controlled by itself. The trigger code is as follows:
/** * ViewPager listener */ val pageChangeListener = object: ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(newState: Int) { } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} override fun onPageSelected(index: Int) {/** * RecyclerView listener */ private val onScrollListener = object: RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {super.onscrolled (recyclerView, dx, dy) /**Copy the code
-
Complex logic:
The exposure conditions of a component depend on the lifecycle of the parent container. For example, in an Activity, the switch between onResume and onPuase needs to be monitored, and the exposure time of the component is terminated when onPause is called. If you have nested fragments, you also need to monitor the Fragment life cycle. In the combination of Activity(Fragment)+ViewPager+Fragment, the Fragment life cycle cannot accurately define whether the current Fragment is visible, which increases the complexity of judging component visibility.
Activity+TabHost(Fragment)+ViewPager+Fragment Therefore, to determine whether the Fragment is visible at the innermost layer, you need to combine the selection of the outer TabHost + the selection of the Fragment +onResume(onPause). The first two conditions are not the life cycle of Android native components and need to be determined by business logic
Component presentation depends on the lifecycle of its own and parent components
solution
The exposure code of a component is difficult to reuse, mainly because the exposure of the component depends on the lifecycle of the parent container, which in turn is affected by the lifecycle of its parent container. The more complex the business presentation, the more complex the exposure logic of the component. So the key to solving the problem is that the exposure logic of the component converges to itself.
Android UI components are ultimately inherited from View, so from a general point of View, it depends on the View’s lifecycle callback. Here are some of the lifecycle methods used
1, onViewAttachToWindow() and onViewDetachedFromWindow()
These two methods come in pairs and represent the current view attached to and detached from the window. It is important to note that onViewAttachToWindow() does not mean that the component is actually on screen, as it may be off-screen at this point.
OnViewAttachToWindow () can be used as a necessary and insufficient condition for exposure, that is, onViewAttachToWindow() does not represent exposure, but exposure does require onViewAttachToWindow() to be called beforehand.
OnViewDetachedFormWindow () can be used as a sufficient and unnecessary condition for the end of exposure, that is, onViewDetachedFormWindow() will definitely cause the end of exposure, But you don’t have to call onViewDetachedFormWindow() at the end of the exposure, such as view.setVisible (view.gone).
2.onWidowsFocusChanged(hasWindowFocus: Boolean)
Called when the window state changes, but in some components this method is not called back when the component is displayed. For example, this method is not called when a custom View in RecyclerView ViewHolder enters the screen, but when the page is closed. View the source code in a ViewGroup dispatchWindowFocusChanged call son View onWidowsFocusChanged method, however, and there is no relevant code in RecyclerView. The View added by calling the addView method outside the lifecycle method in the Android page will not call the onWindowFocusChanged method either, indicating that this method can only be called when triggered by the parent container. Dynamically added views that have already been called by the parent container will not call this method
SetVisibility () will not be called
3.onVisibilityAggregated(isVisible:Boolean)
SetVisibility () is used to handle changes in View visibility caused by view.setvisibility (). This method is called when the visibility of the View itself, its superclass View, or an attached window changes, according to comments in the View, and can be used as a criterion to determine whether the View is visible. But the truth is, onVisibilityAggregated() method calls don’t necessarily prove visibility, aggregated, for example, in off-screen areas. The ViewPager nested Fragment also failed to be called when sliding left or right. Press the Home button, and your app slides back to the background, Android 7, and aggregated View calls onVisibilityAggregated callback, Android 6, View doesn’t go onVisibilityAggregated callback.
It’s hard to judge whether a View can be exposed based on its lifecycle information, and then you can’t prove that a View can be exposed based on one method, and aggregated whether onWindowFocusChanged and onVisibilityAggregated can’t be called!
So you need something else
4.ViewTreeObserver.OnPreDrawListener.onPreDraw()
The onPreDraw method is officially described as a callback function that is executed when the view tree is about to be drawn. That is, when the view changes. This allows us to determine the current position of the component as it moves
5.getLocalVisibleRect(Rect r)
A method in View that gets the current visible area, when called, from r, the current exposed area on the screen. Use the onPreDraw method to get the visible area of a component in real time. However, when the View is InVisible and Gone, it is still visible
6.isShown()
Check whether the current View and parent View are Visible
So far, through the use of the above six methods, you can accurately determine the visibility of the component.
1. When onViewAttachToWindow() is called, you can confirm that the View component is added to the window, but it is not visible
2. The onPreDraw method, getLocalVisibleRect and isShown() methods can accurately obtain the exposure position in the screen, which can ensure that the view does appear in the visible area of the screen
3. OnWindowFocusChanged and onVisibilityAggregated when you jump to a new page, or press the Home button to leave
4. OnVisibilityAggregated when the View’s Visibility changes (View or parent View called View.setvisibility ())
5. OnViewDetachedFormWindow must be called when the page closes Finish
3. Code implementation
/** * Define a Layout as the parent of the ExposureLayout: FrameLayout {/** * define the ExposureHandler class */ private val mExposureHandler by lazy {ExposureHandler(this)} constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet? , defStyleAttr: Int) : super(context, attrs, DefStyleAttr) /** * Add to view */ Override fun onAttachedToWindow() {super.onattachedTowindow () MExposureHandler. OnAttachedToWindow ()} / removed from the view of * * * * / override fun onDetachedFromWindow () { Super. OnDetachedFromWindow () mExposureHandler. OnDetachedFromWindow ()} / * * * view changes in focus * / override fun onWindowFocusChanged(hasWindowFocus: Boolean) { super.onWindowFocusChanged(hasWindowFocus) mExposureHandler.onWindowFocusChanged(hasWindowFocus) } /** * View visibility */ Override Fun onVisibilityAggregated(isVisible: Boolean) {super. OnVisibilityAggregated (isVisible) mExposureHandler. OnVisibilityAggregated (isVisible)} / exposure correction * * * * / fun setExposureCallback(callback: IExposureCallback) {mExposureHandler. SetExposureCallback (the callback)} / * * * set exposure conditions The exposure area size, For example, more than 50% of the display is considered exposure */ ratio: Float) {mExposureHandler. SetShowArea (thewire)} / * * * set minimum exposure time, For example, must be more than two seconds to calculate exposure * / fun setTimeLimit (timeLimit: Int) {mExposureHandler. SetTimeLimit (timeLimit)}}Copy the code
The above code defines a custom Layout that is used as the parent component of the Exposure View. A MexSureHandler is declared as an exposure helper class, and the specific exposure logic is handled in ExposureHandler.
Here is the ExposureHandler implementation code
class ExposureHandler(private val view: View) : ViewTreeObserver. OnPreDrawListener {private var mAttachedToWindow = false / / private var mHasWindowFocus = added to the view of the state Private var mVisibilityAggregated = true // Information about visibility, which aggregated = true, defaults to true, and aggregated Private var mExposure = false private var mExposureCallback: IExposureCallback Private var mStartExposureTime: Long = 0L private var mShowRatio: Float = 0f private var mTimeLimit: Float = 0f Int = 0 For example, 2 seconds (2000) private val mRect = Rect() // Live exposure area /** * Add OnPreDrawListener */ fun onAttachedToWindow() { MAttachedToWindow = true view. ViewTreeObserver. AddOnPreDrawListener (this)} / removed from the view of * * * remove OnPreDrawListener * trying to cancel the exposure * / fun onDetachedFromWindow() { mAttachedToWindow = false view.viewTreeObserver.removeOnPreDrawListener(this) TryStopExposure ()} /** * View focus changes * try unexposing */ fun onWindowFocusChanged(hasWindowFocus: Boolean) {mHasWindowFocus = hasWindowFocus tryStopExposure()} /** * Visibility changes * try to unexpose */ fun onVisibilityAggregated(isVisible: Boolean) {mVisibilityAggregated = isVisible tryStopExposure()} /** * View prepaint * Try to expose when the exposure area met ** Unexpose when the view area didn't meet */ override fun onPreDraw(): Boolean {val visible = view.getLocalVisibleRect(mRect)&&view.isShown // Get visible if (! Visible) {tryStopExposure() return true} if (mShowRatio > 0) {// If there is a limit to the exposure area (kotlin.math.abs(mRect.bottom - mRect.top) > view.height * mShowRatio && kotlin.math.abs(mRect.right - mRect.left) > View.width * mShowRatio) {tryExposure()} else {tryStopExposure()}} else {view.width * mShowRatio) {tryExposure() {tryStopExposure()}} TryExposure () //// return true} /** ** setExposureCallback(callback: IExposureCallback) {mExposureCallback = callback} Float) {mShowRatio = area} /** * Set exposure time limit */ fun setTimeLimit(index: Int) {this.mtimElimit = index} /** * private fun tryExposure() {if (mAttachedToWindow && mHasWindowFocus && mVisibilityAggregated && ! MStartExposureTime = system.currentTimemillis () if (mTimeLimit==0){ mExposureCallback? Private fun tryStopExposure() {if ((! mAttachedToWindow || ! mHasWindowFocus || ! MVisibilityAggregated) && mExposure) {mExposure = false // Reset exposure status if(mTimeLimit >0 && System.currentTimemillis () - MStartExposureTime > mTimeLimit){// mExposureCallback? .show() } } } }Copy the code
The above exposure logic is
1. Judge when mAttachedToWindow, mHasWindowFocus, visibilityaggregated, and getLocalVisibleRect are all aggregated
2. On the basis of 1, determine whether the exposure area limit (mShowRatio) is met to decide whether to expose or cancel exposure
3. On the basis of 2, judge whether the exposure duration limit (mTimeLimit) is met to decide whether to expose or not
Finally, exposure callbacks
Interface IExposureCallback {fun show() // exposure}Copy the code
Based on the above code, the information flow statistics of the home page can be adjusted as follows:
class TimeLineAdapter : RecyclerView.Adapter<TimeLineViewHolder>() { .... override fun onBindViewHolder(holder: MyViewHolder, position: Int) {holder. ExposureLayout. Run {setShowRatio (0.5 f) / / need to be exposed to more than 50% to exposure setTimeLimit (2000)/can/need to display more than two seconds long exposure SetExposureCallback (object: IExposureCallback {override fun show() {// exposure}})}} class TimeLineViewHolder(itemView: View) : recyclerView. ViewHolder(itemView) {// Use ExposureLayout as the parent of the layout: ExposureLayout = itemView.findViewById<ExposureLayout>(R.id.layout_exposure) }Copy the code
4. To summarize
The above exposure logic code can basically meet the exposure requirements of the current business. With a small amount of code, it solves the previous huge and complex exposure logic judgment. Greatly improve the code efficiency. The new statistical code has the following advantages over the previous code:
-
Strong versatility, exposure logic to achieve view-based life cycle, for Android various native components and third-party open source components are applicable, to avoid repeated development code.
-
The code is less intrusive and separated from the business code, which improves the cleanliness of the code
-
Follow-up maintenance is simple and can be used when services change
As the saying goes, grinding a knife does not mistakenly cut wood workers, programmers can not work along the path of predecessors bowed their heads, but also irregular learning thinking, to find a better solution, so as to continue to progress in the work, for the future life into power
One more thing
Snowball business is developing by leaps and bounds, and the engineer team is looking forward to joining us. If you are interested in “being the premier online wealth management platform for Chinese people”, we hope you can join us. Click “Read the article” to check out the hot jobs.
Hot positions: Big front-end architect, Android/iOS/FE technical expert, recommendation algorithm engineer, Java development engineer.