Everyone, can you give me a star for my project or my previous article? It’s so bitter. Github.com Nuggets article

I was asked a technical question during my interview last year.

Interviewer: I heard that you do automatic burying points, so let’s talk about exposure monitoring for view.

Me: BEFORE, I put the exposure monitoring of our advertisement on the model layer of the advertisement, and then made an exposure in BindView, and then made an internal exposure to prevent jitter and avoid multiple exposures.

Interviewer: You mean that quick swiping will also calculate an exposure, if WHAT I need is a stay of more than 1.5 seconds while appearing more than half of the view as a valid exposure.

I:

Let’s have some music in the background.

Interviewer: Go back and wait for the announcement.

After a year of seclusion

To solve the problem, first summarize the problems.

  1. Control appears on the screen for more than 1.5 seconds
  2. More than half of the active area appears

Listen for View moving in and out events

First to solve the RecyclerView 1.5s problem, we may think of the first addOnScrollListener, and then through layoutManager to calculate the visible area, and then calculate the difference interval after two slides. But sorry, I can’t be so easy for you to guess.

    override fun onAttachedToWindow(a) {
        super.onAttachedToWindow()
        exposeChecker.updateStartTime()
    }

    override fun onDetachedFromWindow(a) {
        super.onDetachedFromWindow()
        onExpose()
        exposeChecker.updateStartTime()
    }
Copy the code

I was in a tech blog portal and I saw that these two methods in RecyclerView are triggered when the View moves out of the viewable area. But why?? Analyze the source code with the problem.

Source code analysis

If you’ve ever cared about drawing a view, you probably know these two methods. These two methods will be binding on the page at the time when the window is triggered, the core source host in ViewRootimp dispatchVisibilityAggregated (viewVisibility = = the VISIBLE). When triggered, the host is our Activity’s DecorView.

 mChildHelper = new ChildHelper(new ChildHelper.Callback(){
            @Override
            public void addView(View child, int index) {
                if (VERBOSE_TRACING) {
                    TraceCompat.beginSection("RV addView");
                }
                RecyclerView.this.addView(child, index);
                if (VERBOSE_TRACING) {
                    TraceCompat.endSection();
                }
                dispatchChildAttached(child);
            }
            
            @Override
            public void attachViewToParent(View child, int index,
                    ViewGroup.LayoutParams layoutParams) {
                final ViewHolder vh = getChildViewHolderInt(child);
                if(vh ! =null) {
                    if(! vh.isTmpDetached() && ! vh.shouldIgnore()) {throw new IllegalArgumentException("Called attach on a child which is not"
                                + " detached: " + vh + exceptionLabel());
                    }
                    if (DEBUG) {
                        Log.d(TAG, "reAttach " + vh);
                    }
                    vh.clearTmpDetachFlag();
                }
                RecyclerView.this.attachViewToParent(child, index, layoutParams); }}Copy the code

ChildHelper is a RecyclerView helper class that manages all of its child views. It can be bound together with RecyclerView by exposing the way of interface callback. We can see that when child add and attach both trigger attachViewToParent, the main action is here, and the core source is in the ViewGroup, so let’s move on.

    protected void removeDetachedView(View child, boolean animate) {
        if(mTransition ! =null) {
            mTransition.removeChild(this, child);
        }

        if (child == mFocused) {
            child.clearFocus();
        }
        if (child == mDefaultFocus) {
            clearDefaultFocus(child);
        }
        if (child == mFocusedInCluster) {
            clearFocusedInCluster(child);
        }

        child.clearAccessibilityFocus();

        cancelTouchTarget(child);
        cancelHoverTarget(child);

        if((animate && child.getAnimation() ! =null) || (mTransitioningViews ! =null && mTransitioningViews.contains(child))) {
            addDisappearingView(child);
        } else if(child.mAttachInfo ! =null) {
            child.dispatchDetachedFromWindow();
        }

        if (child.hasTransientState()) {
            childHasTransientStateChanged(child, false);
        }

        dispatchViewRemoved(child);
    }

    protected void attachViewToParent(View child, int index, LayoutParams params) {
        child.mLayoutParams = params;

        if (index < 0) {
            index = mChildrenCount;
        }

        addInArray(child, index);

        child.mParent = this;
        child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
                        & ~PFLAG_DRAWING_CACHE_VALID)
                | PFLAG_DRAWN | PFLAG_INVALIDATED;
        this.mPrivateFlags |= PFLAG_INVALIDATED;

        if (child.hasFocus()) {
            requestChildFocus(child, child.findFocus());
        }
        dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
                && isShown());
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    
    @Override
    boolean dispatchVisibilityAggregated(boolean isVisible) {
        isVisible = super.dispatchVisibilityAggregated(isVisible);
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            // Only dispatch to visible children. Not visible children and their subtrees already
            // know that they aren't visible and that's not going to change as a result of
            // whatever triggered this dispatch.
            if(children[i].getVisibility() == VISIBLE) { children[i].dispatchVisibilityAggregated(isVisible); }}return isVisible;
    }
Copy the code

DispatchVisibilityAggregated which is our most front said ViewRoot ViewGroup triggered by the way, step by step down view distribution view the attach method. When a child control of the RecyclerView is added to the RecyclerView, the attachToWindow method of the child view is triggered.

The rest is where the View detch method is triggered, this is another way to see recyclerview, is tryGetViewHolderForPositionByDeadline.

        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()
                        + exceptionLabel());
            }
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! =null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if(holder ! =null) {
                    if(! validateViewHolderForOffsetPosition(holder)) {// recycle holder (and unscrap if relevant) since it can't be used
                        if(! dryRun) {// we would like to recycle this but need to make sure it is not used by
                            // animation logic etc.
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true; }}}...return holder;
        }
Copy the code

When the ViewHolder should be recycled will trigger RecyclerView tryGetViewHolderForPositionByDeadline this method, Then we can observe that when holder.isScrap() removeDetachedView(holder.itemView, false); And that happens to trigger the viewDetch method of the child.

Solve problem 1.5s problem

After analyzing the above code, we can put the first exposure start node at the end of the onAttachedToWindow method, bury the end of exposure method under the onDetachedFromWindow method, calculate their difference, and call the interface if the value is greater than 1.5 seconds.

View valid area appears more than half

getLocalVisibleRect. This method returns whether the current view appears on the window.


fun View.isCover(a): Boolean {
    var view = this
    val currentViewRect = Rect()
    val partVisible: Boolean = view.getLocalVisibleRect(currentViewRect)
    val totalHeightVisible =
        currentViewRect.bottom - currentViewRect.top >= view.measuredHeight
    val totalWidthVisible =
        currentViewRect.right - currentViewRect.left >= view.measuredWidth
    val totalViewVisible = partVisible && totalHeightVisible && totalWidthVisible
    if(! totalViewVisible)return true
    while (view.parent is ViewGroup) {
        val currentParent = view.parent as ViewGroup
        if(currentParent.visibility ! = View.VISIBLE)//if the parent of view is not visible,return true
            return true

        val start = view.indexOfViewInParent(currentParent)
        for (i in start + 1 until currentParent.childCount) {
            val viewRect = Rect()
            view.getGlobalVisibleRect(viewRect)
            val otherView = currentParent.getChildAt(i)
            val otherViewRect = Rect()
            otherView.getGlobalVisibleRect(otherViewRect)
            if (Rect.intersects(viewRect, otherViewRect)) {
                //if view intersects its older brother(covered),return true
                return true
            }
        }
        view = currentParent
    }
    return false
}

fun View.indexOfViewInParent(parent: ViewGroup): Int {
    var index = 0
    while (index < parent.childCount) {
        if (parent.getChildAt(index) === this) break
        index++
    }
    return index
}

Copy the code

details

We can’t ignore the page switching. When the page is switching, we need to recalculate the exposure of the page. What is the simplest way?

I don’t know if you have been concerned about the onWindowFocusChanged method in viewTree. In fact, this method will be triggered when the page is switched.

The core principle is actually the handleWindowFocusChanged method of ViewRootImp. This method will send down whether to leave the window or not, and then when the iWindow. Stub receives the WMS signal, A message is sent to the ViewRootImp and the lifecycle of the view changes is then distributed down from the ViewRootImp.

  override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
        super.onWindowFocusChanged(hasWindowFocus)
        if (hasWindowFocus) {
            exposeChecker.updateStartTime()
        } else {
            onExpose()
        }
    }
Copy the code

Ouch, we’ll talk about something else when you get back

To sum up, we simply wrap a custom Layout around the ViewHolder and monitor the view’s exposure time using interface callbacks.

I think even if the interview fails, we still need to learn something from it. After all, the opportunity still comes to the prepared person. Of course, as far as I know now, should be hungry me is ali’s control exposure automation buried point scheme, or some different.