onMeasure and onLayout of source code analysis of ViewPager

Posted by mikwit on Thu, 30 Apr 2020 18:56:28 +0200

Reprinted from: https://www.jianshu.com/p/b0830f9b44bb

We know that the ViewPager is a ViewGroup. When we customize the ViewGroup ourselves, in addition to writing at least two constructors, the onMeasure and onLayout functions basically have to be written. Today, I will study two functions of onMeasure and onLayout carefully~

onMeasure

ViewPager divides sub views into two types. One is Decor View used to decorate ViewPager, which may take up some space; the other is common sub View, that is, each View displayed when we slide horizontally. onMeasure first measures the decor View, and then the common sub View. The detailed notes are as follows:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //Set the size information according to the layout file. The default size is 0
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec));

    final int measuredWidth = getMeasuredWidth();
    final int maxGutterSize = measuredWidth / 10;
    //Set the value of mGutterSize, which will be discussed later
    mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

    // The display area of ViewPager can only be displayed for one View
    //childWidthSize and childHeightSize are the available width height sizes of a View
    //That is, the width and height after the internal margin of ViewPager are removed
    int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
    int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

    //1. Measure Decor View first
    //The following loop is only for Decor View, that is, View used to decorate ViewPager
    int size = getChildCount();
    for (int i = 0; i < size; ++i) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //1.1 if the View is Decor View, it is used to decorate the View pager
            if (lp != null && lp.isDecor) {
                //1.2 get the Gravity in horizontal direction and vertical direction of Decor View
                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //1.3 the width and height corresponding to the default Dedor View mode is wrap < content
                int widthMode = MeasureSpec.AT_MOST;
                int heightMode = MeasureSpec.AT_MOST;

                //1.4 record whether Decor View occupies space vertically or horizontally
                boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;

                //1.5 consumeHorizontal: if the space is occupied in the vertical direction,
                // Then the horizontal direction is match [parent, i.e. EXACTLY
                //How much space is occupied in the vertical direction depends on Decor View
                //The same goes for consumeHorizontal
                if (consumeVertical) {
                    widthMode = MeasureSpec.EXACTLY;
                } else if (consumeHorizontal) {
                    heightMode = MeasureSpec.EXACTLY;
                }
                //1.6 width and height, initialized as the available space of sub View in ViewPager visual area
                int widthSize = childWidthSize;
                int heightSize = childHeightSize;
                //1.7 if width is not wrap [content], the measurement mode of width is EXACTLY
                //If the width is neither wrap content nor match parent, then it is the user
                //Set the widthSize to the specific size written in the layout file
                if (lp.width != LayoutParams.WRAP_CONTENT) {
                    widthMode = MeasureSpec.EXACTLY;
                    if (lp.width != LayoutParams.FILL_PARENT) {
                        widthSize = lp.width;
                    }
                }
                //1.8 is the same as 1.7
                if (lp.height != LayoutParams.WRAP_CONTENT) {
                    heightMode = MeasureSpec.EXACTLY;
                    if (lp.height != LayoutParams.FILL_PARENT) {
                        heightSize = lp.height;
                    }
                }
                //1.9 width height specification of composite Decor View (integer including size and mode)
                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                //1.10 measure DecorView
                child.measure(widthSpec, heightSpec);
                //1.11 if Decor View occupies the space in the vertical direction of ViewPager
                //You need to subtract the height of DecorView from the space available in the vertical direction of the child View,
                //In the same way, do the same in the horizontal direction
                if (consumeVertical) {
                    childHeightSize -= child.getMeasuredHeight();
                } else if (consumeHorizontal) {
                    childWidthSize -= child.getMeasuredWidth();
                }
            }
        }
    }

    //2. specification of default width and height of subview (including integer of size and mode)
    //(PS: mChildWidthMeasureSpec is not used again, I feel it is a bit redundant)
    mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
    mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

    //3. Make sure that the fragment s we need to display have been created by us
    //populate() is more complex, which will be described in detail later
    mInLayout = true;
    populate();
    mInLayout = false;

    // 4. Measure the sub View again
    size = getChildCount();
    for (int i = 0; i < size; ++i) {
        final View child = getChildAt(i);
        //4.1 no need to measure if visibility is fine
        if (child.getVisibility() != GONE) {
            if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
                    + ": " + mChildWidthMeasureSpec);
            //4.2 get LayoutParams of child View
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //4.3 only measure for sub View instead of Decor View
            if (lp == null || !lp.isDecor) {
                //4.4 the widthFactor of layoutparams is a floating-point number with a value of [0,1],
                // Used to indicate the ratio of subview to the available width of subview in the ViewPager display area,
                // That is, (childWidthSize * lp.widthFactor) represents the actual width of the current subview
                final int widthSpec = MeasureSpec.makeMeasureSpec(
                        (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                //4.5 measure the current subview
                child.measure(widthSpec, mChildHeightMeasureSpec);
            }
        }
    }
}

onLayout

We know that the child views of ViewPager are placed horizontally, so in onLayout, most of the work is to calculate childLeft, that is, the left position of child views, while the top position is basically the same, which can be reflected in the following code:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //1. The following local variables are very simple and will not be explained
    final int count = getChildCount();
    int width = r - l;
    int height = b - t;
    int paddingLeft = getPaddingLeft();
    int paddingTop = getPaddingTop();
    int paddingRight = getPaddingRight();
    int paddingBottom = getPaddingBottom();
    final int scrollX = getScrollX();
    //2. Number of decor views
    int decorCount = 0;

    //3. First, layout the Decor View, and then layout the common sub View,
    // The reason why the Decor View is laid out first is to make the normal sub View have appropriate offset
    //The following loop is mainly for Decor View
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        //3.1 layout only when visibility is not GONE
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //3.2 the margins on the left and top are initialized to 0
            int childLeft = 0;
            int childTop = 0;
            if (lp.isDecor) {//3.3 Decor View only
                //3.4 get Gravity in horizontal or vertical direction
                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //3.5 determine childLeft and paddingRight according to Gravity in horizontal direction
                switch (hgrav) {
                    default://When the horizontal direction Gravity is not set (left, middle and right), childLeft takes paddingLeft
                        childLeft = paddingLeft;
                        break;
                    case Gravity.LEFT://Horizontal Gravity is left, Decor View is to the left
                        childLeft = paddingLeft;
                        paddingLeft += child.getMeasuredWidth();
                        break;
                    case Gravity.CENTER_HORIZONTAL://Center Decor View
                        childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
                                paddingLeft);
                        break;
                    case Gravity.RIGHT://Move Decor View to the far right
                        childLeft = width - paddingRight - child.getMeasuredWidth();
                        paddingRight += child.getMeasuredWidth();
                        break;
                }
                //3.6 is the same as 3.5
                switch (vgrav) {
                    default:
                        childTop = paddingTop;
                        break;
                    case Gravity.TOP:
                        childTop = paddingTop;
                        paddingTop += child.getMeasuredHeight();
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = Math.max((height - child.getMeasuredHeight()) / 2,
                                paddingTop);
                        break;
                    case Gravity.BOTTOM:
                        childTop = height - paddingBottom - child.getMeasuredHeight();
                        paddingBottom += child.getMeasuredHeight();
                        break;
                }
                //3.7 the childLeft calculated above is relative to the left side of ViewPager,
                //You also need to add the distance scrollX that has slipped in the x direction
                childLeft += scrollX;
                //3.8 layout of Decor View
                child.layout(childLeft, childTop,
                        childLeft + child.getMeasuredWidth(),
                        childTop + child.getMeasuredHeight());
                //3.9 add Decor View quantity + 1
                decorCount++;
            }
        }
    }
    //4. Width of common subview
    final int childWidth = width - paddingLeft - paddingRight;
    // Page views. Do this once we have the right padding offsets from above.
    //5. For the normal sub View layout, we have got the correct offset before that
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //5.1 ItemInfo is a static internal class of ViewPager,
            // It stores the position, offset and other information of common subview, which is an abstract description of common subview
            ItemInfo ii;
            //5.2 infoForChild queries the corresponding ItemInfo object through the incoming View
            if (!lp.isDecor && (ii = infoForChild(child)) != null) {
            //Calculate the left offset of the current subview
                int loff = (int) (childWidth * ii.offset);
                //Left margin + left offset to get the left position of the final subview
                int childLeft = paddingLeft + loff;
                int childTop = paddingTop;
                //5.3 if the current subview needs to measure, when the subview is newly added during Layout,
                // Then this sub View needs to be measured, that is, needsMeasure is true
                if (lp.needsMeasure) {
                    //5.4 marking has been measured
                    lp.needsMeasure = false;
                    //5.5 the following process is similar to onMeasure
                    final int widthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (childWidth * lp.widthFactor),
                            MeasureSpec.EXACTLY);
                    final int heightSpec = MeasureSpec.makeMeasureSpec(
                            (int) (height - paddingTop - paddingBottom),
                            MeasureSpec.EXACTLY);
                    child.measure(widthSpec, heightSpec);
                }
                if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
                        + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
                        + "x" + child.getMeasuredHeight());
                //5.6 layout common subviews
                child.layout(childLeft, childTop,
                        childLeft + child.getMeasuredWidth(),
                        childTop + child.getMeasuredHeight());
            }
        }
    }
    //6. Save some local variables to instance variables
    mTopPageBounds = paddingTop;
    mBottomPageBounds = height - paddingBottom;
    mDecorChildCount = decorCount;
    //7. If it is the first layout, slide the ViewPager to the position of the first child View
    if (mFirstLayout) {
        scrollToItem(mCurItem, false, 0, false);
    }
    //8. The mark has been laid out, that is, it is no longer the first time
    mFirstLayout = false;
}

OK, the most basic onMeasure and onLayout have been analyzed, and they can't slide now. Next, I will analyze the slide of ViewPager~

Topics: Fragment