preface

Recently, I have been focusing on the use of ViewPager2, during the official Demo has been debugging Android-ViewPager2, today encountered a weird problem, elusive for a long time and finally found the reason, the original is the layout of the Demo problem, afterwards I feel it is necessary to share this process, On one hand, it can consolidate the knowledge of View measurement, and on the other hand, I hope you can avoid this pit.

Reading guide

  • Code based on Android-ViewPager2, see the official master had better download the source code experience;

Into the hole site

To see the lifecycle of the Fragment, I buried the lifecycle method in the CardFragment class.

Procedure For an exception to occur:

Landscape screen into the CardFragmentActivity or CardFragmentActivity portrait screen cut to landscape screen, the console instantly print multiple fragments of the life cycle Log, the scene is amazing;

CardFragmentActivity Landscape layout

Console Log output

OnCreate ->onDestory onCreate->onDestory onCreate->onDestory onCreate->onDestory onDestory onCreate->onDestory onDestory

Before the operation, only Fragment2 is created and displayed. Theoretically, after rotating the screen, only Fragment2 is destroyed and rebuilt. No other fragments are called. Now the problem is that after the rotation, a bunch of fragments are created and destroyed, and only Fragment2 is left. This is definitely a Bug, although it happened in the official Demo where no single line of code was changed.

The preliminary reasonsThe MATCH_PARENT calculation is invalid

ViewPager2 currently only supports MATCH_PARENT as the ItemView layout parameter, which is the effect of filling in the parent layout; Since ViewPager2 is based on RecyclerView, theoretically each ItemView must be MATCH_PARENT, controlling a screen to load only one Item, but once MATCH_PARENT calculation is invalid, Then ViewPager2 is basically the effect of RecyclerView, instant multiple fragments can be explained through;

ViewPager2Measurement process

ViewPager2

@Override protected void onMeasure(int widthMeasureSpec, Int heightMeasureSpec) {// measure mRecyclerView (mRecyclerView, widthMeasureSpec, heightMeasureSpec); int width = mRecyclerView.getMeasuredWidth(); int height = mRecyclerView.getMeasuredHeight(); int childState = mRecyclerView.getMeasuredState(); Width += getPaddingLeft() + getPaddingRight(); height += getPaddingTop() + getPaddingBottom(); Suggestedminimumwidth (); suggestedMinimumWidth (); height = Math.max(height, getSuggestedMinimumHeight()); // Set self heightsetMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
            resolveSizeAndState(height, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
}
Copy the code

ViewPager2. OnMeasure () calculates the size of McLerview first, so the focus of attention shifted to the recyclerView.onmeasure (), RecyclerView calculates and layout logic for subviews in LayoutManager, So important to see this example LinearLayoutManager, pair LayoutManager View calculation method is measureChildWithMargins (), look at the below measureChildWithMargins () method call stack;

MeasureChildWithMargins ()

RecyclerView.LayoutManager

public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); / / gets the current View of Decor (line) of traditional idea size final the Rect insets. = mRecyclerView getItemDecorInsetsForChild (child); widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; Final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, canScrollHorizontally()); Final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, canScrollVertically()); // If a measurement is needed, call child's measurement methodif(shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); }}Copy the code

Code for obtaining width and height measurements:

public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
        int childDimension, boolean canScroll) {
    int size = Math.max(0, parentSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    if (canScroll) {
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            switch (parentMode) {
                case MeasureSpec.AT_MOST:
                case MeasureSpec.EXACTLY:
                    resultSize = size;
                    resultMode = parentMode;
                    break;
                case MeasureSpec.UNSPECIFIED:
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                    break; }}else if(childDimension == LayoutParams.WRAP_CONTENT) { resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; }}else{// omit}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
Copy the code

Parse the getChildMeasureSpec() method, and since ViewPager2 forces MATCH_PARENT, childDimension must be MATCH_PARENT, so what’s the resultMode? Print it out with a breakpoint, ParentMode here is measurespec. UNSPECIFIED and Measurespec. EXACTLY alternate; parentMode is measurespec. UNSPECIFIED and MeasureSpec.

MeasureSpec.UNSPECIFIED and MeasureSpec.EXACTLY appear alternately, and finally print the onMeasure output of RecyclerView directly.

RecyclerView. OnMeasure Output logs

In portrait mode, widthMeasureMode is always 1073741824(MATCH_PARENT), but in landscape mode, widthMeasureMode hovered between 0(UNSPECIFIED) and MATCH_PARENT; MeasureMode = UNSPECIFIED; MeasureMode = UNSPECIFIED; MeasureMode = UNSPECIFIED; MeasureMode = UNSPECIFIED;

How it came aboutUNSPECIFIED?

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:background="#FFFFFF">
    <include layout="@layout/controls" />
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

</LinearLayout>
Copy the code

So the overall layout is a LinearLayout, inside the layout, ViewPager2 layout_width=”0dp” layout_weight=”1″, maybe width=0dp && weight=1, Skimping a LinearLayout to measure code logic;

LinearLayout

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else{ measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}Copy the code

OnMeasure () method of LinearLayout is divided into vertical direction and horizontal direction. Here, we choose measureHorizontal() to start with.

In measureHorizontal() method, it is determined whether the interim load useExcessSpace is required by judging lp.width == 0 && lp.weight > 0. The interim load below is measured in the mode of UNSPECIFIED;

Why do I have to do it againMATCH_PARENTmeasurement

This is because measureHorizontal() of the LinearLayout will be measured twice for the layout of the overloading useExcessSpace, and the second time will transmit the actual measurement mode;

whyUNSPECIFIEDMode,MATCH_PARENTWill the failure

We only discuss the case of FrameLayout for the moment. If the parent layout of FrameLayout gives the measurement mode of the FrameLayout as UNSPECIFIED, its size is its specific width and height, FrameLayout’s LayoutParams are MATCH_PARENT. Can FrameLayout measure the exact size of MATCH_PARENT?

FrameLayout

FrameLayout will measure the size of all visible views, and then calculate the maximum size maxWidth and maxHeight. To measure its own size, call setMeasureDimension (). The resolveSizeAndState(maxWidth, widthMeasureSpec, childState) method is called for each Dimension setting;

resolveSizeAndState()

public static int resolveSizeAndState(int size, int measureSpec, Int childMeasuredState) {// Final int specMode = measurespec.getMode (MeasureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; Switch (specMode) {// Parent layout suggested modecase MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        caseMeasureSpec. UNSPECIFIED: / / default: here the result = size; // This is the size passed in}return result | (childMeasuredState & MEASURED_STATE_MASK);
}
Copy the code

MeasureSpec’s specMode is UNSPECIFIED, and the result returns the size passed in, which in FrameLayout is maxWidth and maxHeight. MeasureSpec’s specMode is UNSPECIFIED, and the result returns the size as passed in. MeasureSpec’s specMode is UNSPECIFIED. It’s not the specSize given by parent;

Why is the whole thing measured twice

This is due to the FrameLayout for MATCH_PARENT layout, the second measurement, the first measurement in order to find the maximum size maxsize, the second measurement will use maxsize to recalculate the child View of MATCH_PARENT;

Avoid into the pit

UNSPECIFIED, the measure affects MATCH_PARENT, at least on FrameLayout, which takes the maximum size of the child View, and once the meaning of MATCH_PARENT is lost, ViewPager2 loses the ability to display an ItemView on one screen, so you’ll see a Fragment burst.

As ViewPager2 is used with Fragment, the root layout is UNSPECIFIED. The solution is to not allow dimension measurements with the same slide direction, which appear as UNSPECIFIED;

If the parent layout is a LinearLayout, you want to avoid layout_width=”0dp” and layout_weight=”1″ when you’re sliding horizontally, or layout_height=”0dp” and layout_weight=”1″ when you’re sliding vertically, The solution to this code is simply to drop layout_weight=”1″ and set layout_width to match_parent;

conclusion

Note that when ViewPager2 is used with fragments, once the Fragment is found to increase immediately, it may be because the Item size measurement is incorrect, which causes this reason, it must be first thought of as UNSPECIFIED, · If the LinearLayout may be the reason for layout_weight=”1″, similarly, RecyclerView+PagerSnapHelper+match_parent to achieve a screen of one Item scheme, there is also this risk;