The requestLayout() method of a View is used to trigger a layout action, usually when we change some parameters that affect the layout of the View. The common usage is as follows:

view.layoutParams.apply{
    width = 100
    height = 200
}
view.requestLayout()
Copy the code

To analyze why the call failed, we first need to understand the requestLayout() process.

RequestLayout invokes the process

How do YOU start a layout after calling requestLayout()? Let’s look at requestLayout() source code:

public void requestLayout(a) {
    if(mMeasureCache ! =null) mMeasureCache.clear();

    if(mAttachInfo ! =null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if(viewRoot ! =null && viewRoot.isInLayout()) {
            if(! viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if(mParent ! =null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if(mAttachInfo ! =null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null; }}Copy the code

MeasureCache = MeasureCache = MeasureCache = MeasureCache = MeasureCache = MeasureCache The next section of code should be request-during layout according to the logic of the comment, which we’ll skip later. The code that follows is the core of the method: setting the draw state bits and calling parent’s requestLayout. PFLAG_FORCE_LAYOUT indicates that the current View needs a layout, which means that the View’s current layout data is out of date and needs to be reconfigured with the next layout pass. PFLAG_INVALIDATED is similar to PFLAG_FORCE_LAYOUT except that it draws data. The parent’s requestLayout() function is called because the parent View contains the child View, and the layout of the child View determines the layout of the parent View. So when the child View layout changes also need to notify the parent View to refresh its own layout. The ViewRootImpl requestLayout() method is finally called by calling it level by level, which looks like this:

if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true;
    scheduleTraversals();
}
Copy the code

The logic is simple. Layout should be done in the scheduleTraversals() method, and by following the invocation relationship, the performLayout() method is called in the DecorView’s Layout() method, Start the familiar layout process by calling the layout() method of the child View from the top down, in the opposite direction to requestLayout() :

Request – during – layout processing

Now let’s look at the View. RequestLayout () just skip part, here by mAttachInfo. MViewRequestingLayout variable to determine the launch requestLayout () View, Because only the initiating View can trigger request-during-layout logic, its ancestor Views cannot, for reasons discussed later.

Request-during – Layout is a View that calls requestLayout() during a layout pass. Code can be seen through viewRoot. IsInLayout () to judge whether the current in the layout, and then call ViewRootImpl. RequestLayoutDuringLayout method, we continue to see this method:

.if (!mLayoutRequesters.contains(view)) {
    mLayoutRequesters.add(view);
}
...
Copy the code

The core logic is the above sentence, add the initiating View to the mLayoutRequesters list of ViewRootImpl. To see when to use this list, look at the code to see where it is used. In performLayout(), the first line of code is mInLayout = false, indicating that the list was processed after the last Layout pass. The logic is simple: filter the list to get valid views, and then call requestLayout() in turn.

Request-during -layout calls are deferred until the current layout pass is completed. This explains why the request-during layout logic is triggered only when the View is initiated.

RequestLayout () call invalid cause

According to requestLayout () process can be found, if from the bottom up call interrupt cannot be transferred to the ViewRootImpl. RequestLayout () will cause cannot refresh layout. The parent View’s requestLayout() needs to meet two requirements. = null and! Parent.islayoutrequested (), if parent is empty it means that the View is not on and does not need to refresh the layout, which is a reasonable condition.

Another condition indicates that the parent has already called requestLayout(), which is used to prevent the next layout from starting without the ongoing layout having finished. But what if we do need to refresh the layout of the current interface? No, the View designer thought of this situation, and the solution is request-During layout above.

Request-during -layout handling is not foolproof, however, and there are two bugs that can cause requestLayout() calls to fail:

  1. Request-during -layout must be handled when view. isInLayout == true. If the current is not in the layout pass and requestLayout () invocation chain cannot function to ViewRootImpl. RequestLayout () call will fail. IsLayoutRequested () == false (View. IsLayoutRequested () == true) Of course not. Let’s look at the view.isinLayout () code first:

    public boolean isInLayout(a) {
        ViewRootImpl viewRoot = getViewRootImpl();
        return(viewRoot ! =null && viewRoot.isInLayout());
    }
    Copy the code

    You can see that isInLayout() depends on viewrootimpl.isinLayout (). Continue with this method:

    boolean isInLayout(a) {
       return mInLayout;
    }
    Copy the code

    While mInLayout = true only in ViewRootImpl. PerformLayout (), in other words, only the layout of the trigger to refresh this method will make the isInLayout () = = true, This requestLayout() call will fail if the layout refresh is triggered in another way. Specific what layout refresh call. Not by ViewRootImpl performLayout () initiated? At present, one situation encountered is the layout refresh of itemView caused by sliding pages in RecyclerView. Specifically, it is when sliding itemView outside the interface into the interface. An example of a call stack is as follows:

    . at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1818) at android.widget.LinearLayout.onLayout(LinearLayout.java:1584) at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673) at android.widget.LinearLayout.onLayout(LinearLayout.java:1582) at android.view.View.layout(View.java:22254) at android.view.ViewGroup.layout(ViewGroup.java:6310) at androidx.recyclerview.widget.RecyclerView$LayoutManager.layoutDecoratedWithMargins(RecyclerView.java:9322)
    at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1615)
    at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1517)
    at androidx.recyclerview.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1331)
    at androidx.recyclerview.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1075)
    at androidx.recyclerview.widget.RecyclerView.scrollStep(RecyclerView.java:1832)
    at androidx.recyclerview.widget.RecyclerView.scrollByInternal(RecyclerView.java:1927)
    at androidx.recyclerview.widget.RecyclerView.onTouchEvent(RecyclerView.java:3187)
    ...
    at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:448)
    at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1840)
    at android.app.Activity.dispatchTouchEvent(Activity.java:3873)
    at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
    at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
    at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:406)
    at android.view.View.dispatchPointerEvent(View.java:14056)
    ...
    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7621)
    ...
    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188)
    ...
    Copy the code

    From the above call stack can clearly see the InputEvent – > TouchEvent – > RecyclerView. Scroll * () – > LinearLayoutManager. Scroll * () – > LinearLayoutManager. LayoutChunk () – > itemView. The layout () such a call flow, itemView here is in the process of layout, But not ViewRootImpl. PerformLayout launched, so the isInLayout () = = false, will trigger this call we fail. Is the View designer responsible for this bug? I don’t think so. The behavior of scrolling to trigger a layout is a special RecyclerView process, and the invalidation of requestLayout() calls caused by this special RecyclerView process should be handled by the requestLayout() process, which obviously isn’t.

  2. Even if request-during-layout can be triggered, the originating View must be filtered once before a deferred call to requestLayout(), and the View and its ancestor must not be visibility GONE. And set up the PFLAG_FORCE_LAYOUT state, the corresponding code in ViewRootImpl. GetValidLayoutRequesters (). The first filter condition is understandable: invisible views do not require a layout. The second may invalidate the call. This state indicates whether the layout needs to be rearranged. This state is enabled when requestLayout() is called and cleared when layout is complete. For example, if view.pFLAG_force_layout is set by calling requestLayout() in a layout, then the flag bit is cleared before request-during layout is processed. Is that possible? Yes, the code is as follows:

    view.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> 
    	v.layoutParams.width = 100
    	v.requestLayout()
    }
    Copy the code

    RequestLayout () in the above code won’t work, why? Let’s see where onLayoutChangeListener is called:

    public void layout(int l, int t, int r, int b) {...if(li ! =null&& li.mOnLayoutChangeListeners ! =null) {
    		ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
    		int numListeners = listenersCopy.size();
    		for (int i = 0; i < numListeners; ++i) {
    			listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); }}final booleanwasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; . }Copy the code

    As you can see from the code above, PFLAG_FORCE_LAYOUT is cleared when onLayoutChangeListener is called.

The solution

Now that you know why requestLayout() is failing, what can you do to fix it? The specific code is as follows:

fun View.isSafeToRequestDirectly(a):Boolean {
	return if (isInLayout) {
        // when isInLayout == true and isLayoutRequested == true,
        // means that this layout pass will layout current view which will
        // make currentView.isLayoutRequested == false, and this will let currentView
        // ignored in process handling requests called during last layout pass.
        isLayoutRequested.not()
    } else {
        var ancestorLayoutRequested = false
        var p: ViewParent? = parent
        while(p ! =null) {
            if (p.isLayoutRequested) {
                ancestorLayoutRequested = true
                break
            }
            p = p.parent
        }
        ancestorLayoutRequested.not()
    }
}

fun View.safeRequestLayout(a) {
    if (isSafeToRequestDirectly()) {
    	requestLayout()
    } else {
    	post { requestLayout() }
    }
}
Copy the code

Issafetorequestlayout () is used to determine whether calling requestLayout() is effective. IsInLayout == true/false is used to determine whether requestLayout() is effective. If so, call it directly; otherwise, delay the call with the post() method until the current layout is finished.