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.
- Control appears on the screen for more than 1.5 seconds
- 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.