Android 自定义 View——requestLayout 的传递与测量、布局流程分析

2,951 阅读14分钟

在之前的invalidate传递与绘制流程分析文章中我们对invalidate的流程进行了详细分析,现在来继续分析下requestLayout的流程吧

一、requestLayout的请求传递

我们从View#requestLayout方法的源码开始

public void requestLayout() {
    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;
    }
    // 增加PFLAG_FORCE_LAYOUT标记,在measure时会校验此属性
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
    // 父类不为空&&父类没有请求重新布局(是否有PFLAG_FORCE_LAYOUT标志)
    if (mParent != null && !mParent.isLayoutRequested()) {
        // 调用父类的requestLayout
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

在源码中,会对measure的缓存进行清除,之后会判断ViewTree是否正在布局流程,接着为View设置标记,PFLAG_FORCE_LAYOUT标记会在View进行measure时验证。之后非常重要会判断父类是否为空以及检查是否正在请求重新布局(即检查之间设置的PFLAG_FORCE_LAYOUT标记),如果满足条件则会父类的requestLayout方法,而ViewGroup继承自View,其requestLayout方法调用了View的requestLayout方法,所以会不断迭代的调用父类的requestLayout方法,直到DecorView的父类ViewRoot。

ViewRoot的实现类为ViewRootImpl,在这个类中的requestLayout方法与View中的并不相同,我们来看下他的源码

@Override
public void requestLayout() {
    // 是否在处理requestLayout
    if (!mHandlingLayoutInLayoutRequest) {
        // 检查创建view的线程是否为当前线程
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

在ViewRootImpl#requestLayout中,首先判断是否正在requestLayout中,之后检查当前线程是否为创建View的线程。接着调用scheduleTraversals方法,发起请求。

ViewRootImpl#scheduleTraversals方法调用performTraversals方法的过程已经在自定义View——invalidate流程分析中进行了详细说明,在这里就不在赘述了。

private void performTraversals() {
    ...
    Rect frame = mWinFrame;
    if (mFirst) {
        mFullRedrawNeeded = true;
        mLayoutRequested = true;
        if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
            // NOTE -- system code, won't try to do compat mode.
            Point size = new Point();
            mDisplay.getRealSize(size);
            desiredWindowWidth = size.x;
            desiredWindowHeight = size.y;
        } else {
            DisplayMetrics packageMetrics =
                mView.getContext().getResources().getDisplayMetrics();
            desiredWindowWidth = packageMetrics.widthPixels;
            desiredWindowHeight = packageMetrics.heightPixels;
        }
        // We used to use the following condition to choose 32 bits drawing caches:
        // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
        // However, windows are now always 32 bits by default, so choose 32 bits
        mAttachInfo.mUse32BitDrawingCache = true;
        mAttachInfo.mHasWindowFocus = false;
        mAttachInfo.mWindowVisibility = viewVisibility;
        mAttachInfo.mRecomputeGlobalAttributes = false;
        viewVisibilityChanged = false;
        mLastConfiguration.setTo(host.getResources().getConfiguration());
        mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
        // Set the layout direction if it has not been set before (inherit is the default)
        if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
            host.setLayoutDirection(mLastConfiguration.getLayoutDirection());
        }
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
        //Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn);
    } else {
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(TAG,
                    "View " + host + " resized to: " + frame);
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            windowSizeMayChange = true;
        }
    }

    ...

    if (mFirst || windowShouldResize || insetsChanged ||
            viewVisibilityChanged || params != null) {
        ...
        if (!mStopped || mReportNextDraw) {
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed!  mWidth="
                        + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                        + " mHeight=" + mHeight
                        + " measuredHeight=" + host.getMeasuredHeight()
                        + " coveredInsetsChanged=" + contentInsetsChanged);

                 // Ask host how big it wants to be
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                // Implementation of weights from WindowManager.LayoutParams
                // We just grow the dimensions as needed and re-measure if
                // needs be
                int width = host.getMeasuredWidth();
                int height = host.getMeasuredHeight();
                boolean measureAgain = false;

                if (lp.horizontalWeight > 0.0f) {
                    width += (int) ((mWidth - width) * lp.horizontalWeight);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }
                if (lp.verticalWeight > 0.0f) {
                    height += (int) ((mHeight - height) * lp.verticalWeight);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                            MeasureSpec.EXACTLY);
                    measureAgain = true;
                }

                if (measureAgain) {
                    if (DEBUG_LAYOUT) Log.v(TAG,
                            "And hey let's measure once more: width=" + width
                            + " height=" + height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                layoutRequested = true;
            }
        }
    } else {
        ...
    }

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    boolean triggerGlobalLayoutListener = didLayout
            || mAttachInfo.mRecomputeGlobalAttributes;
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
}

上面截取了performTraversals的主要相关代码,在其中一般情况下蒋会执行performMeasure、performLayout、performDraw三个方法。调用performMeasure方法之前,会先分别对焦点、测量的宽高、View内容的变化情况进行判断,如果变化则会执行performMeasure。接着会检查窗口属性是否包含weight,如果包含蒋会再执行一次performMeasure方法,在之后会设置layoutRequested = true,表示需要重新布局。在执行performLayout方法之前,会对didLayout参数进行检查,判断是否请求重新布局,窗口是否停止,是否需要再次绘制,而layoutRequested参数再performMeasure后设置为true,mStopped默认为false,所以将执行performMeasure方法。performDraw方法就像在自定义View——invalidate流程分析中分析的一样,蒋会执行,但是在ViewRootImpl#draw中进行dirty判断时,会发现dirty为空,所以不会继续执行绘制过程。那么一般情况下的进行requestLayout请求后,view的重新绘制在什么地方呢?这将会在稍后的layout过程中看到答案。

二、测量流程

在查看performMeasure的源码之前,我们先看看传入performMeasure的两个参数childWidthMeasureSpec, childHeightMeasureSpec的获取,代码中可以看出参数是通过调用getRootMeasureSpec方法来获得,现在来看下getRootMeasureSpec的源码

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

通过上述getRootMeasureSpec的代码,就可以清楚的看出LayoutParams与测量模式的对应关系。

LayoutParams Mode size
MATCH_PARENT EXACTLY windowSize
WRAP_CONTENT AT_MOST windowSize
固定大小 EXACTLY rootSize

我们继续看看performMeasure方法

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

从上述代码可以看出,将会执行View的measure方法,measure方法中将会执行onMeasure方法,ViewRootImpl的调用的view为DecorView(DecorView为布局的顶层view),现在来看看DecorView#onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
    final boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
    final int widthMode = getMode(widthMeasureSpec);
    final int heightMode = getMode(heightMeasureSpec);
    boolean fixedWidth = false;
    if (widthMode == AT_MOST) {
        final TypedValue tvw = isPortrait ? mFixedWidthMinor : mFixedWidthMajor;
        // tvw不会为NULL,等级也不会为NULL,具体原因可以跟踪一下源码。
        if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {
            final int w;
            // 获取视图宽度
            if (tvw.type == TypedValue.TYPE_DIMENSION) {
                w = (int) tvw.getDimension(metrics);
            } else if (tvw.type == TypedValue.TYPE_FRACTION) {
                w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);
            } else {
                w = 0;
            }
            // 设置测量模式为EXACTLY
            if (w > 0) {
                final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        Math.min(w, widthSize), EXACTLY);
                fixedWidth = true;
            }
        }
    }
    // heightMode的处理方式与widthMode相同
    if (heightMode == AT_MOST) {
        ...
    }
    ...
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

上述代码中,将会分别检查宽高测量模式,这里以宽度测量属性为例,首先检查测量模式是否为AT_MOST,如果是,则获取视图的宽度,然后与宽度测量属性的大小取小,接着与测量模式EXACTLY,作为MeasureSpec.makeMeasureSpec方法的参数一起生成新的宽度测量属性。之后会把新生成的测量属性传递给DecorView的父类,也就是FrameLayout的onMeasure方法继续处理。我们来看看它是怎么做的。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            // 获取子view中最大的宽度和高度
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                // 如果子View的宽or高为MATCH_PARENT,则保存子View
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
    // 保存测量结果
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    count = mMatchParentChildren.size();
    // 对之前保存的子view,分别重新测量MeasureSpec
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }
            final int childHeightMeasureSpec;
            if (lp.height == LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }
            // 测量子View
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

FrameLayout的测量过程中,首先遍历子View,调用measureChildWithMargins方法,之后获取子view中的最大宽度or高度,这是因为FrameLayout的布局,如果在wrap_content的情况下,其宽度就等于所以子View中的最大宽度,高度就等于所以子View中最大的高度。然后对子View的宽or高为MATCH_PARENT的View进行存储。之后处理一些属性,保存测量结果。resolveSizeAndState方法,我已经在之前的自定义View——雷达图(蜘蛛网图)中进行过分析。最后就是对之前保存的子view进行处理了。从measureChildWithMargins方法的参数可以看出,测量值与view的测量值以及子view相关,现在我们来看下它的测量过程

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上述方法中,会先调用getChildMeasureSpec方法,获取测量的宽高属性之后,最后对子View进行测量。由代码可以看出测量属性的获取与父view的MeasureSpec、View的padding、子View的LayoutParams相关,具体的关系我们来看看ViewGroup的getChildMeasureSpec方法

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述方法并不难,写的如此有规律,它主要是根据View的MeasureSpec与子View的LayoutParams参数来确定子View的MeasureSpec属性。接下来,我们为getChildMeasureSpec方法的逻辑建立一个表格。

getChildMeasureSpec

现在我们回到measureChildWithMargins方法,测量完成之后,就是对子View的测量,DecorView的子View就是我们平时setContentView中的布局,这里以LinearLayout为例。自然也是和之前一样LinearLayout的measure调用onMeasure,直接来看看LinearLayout#onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

上述代码可以看出,LinearLayout将根据属性来选择一种测量方式,我们选择LinearLayout的水平布局的测量方式,即measureHorizontal,这里简单的挑选其中的主要部分说明一下

void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    // See how wide everyone is. Also remember max height.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        ...
        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                child.getLayoutParams();
        // 计算比重
        totalWeight += lp.weight;

        if (widthMode == MeasureSpec.EXACTLY && lp.width == 0 && lp.weight > 0) {
            // Optimization: don't bother measuring children who are going to use
            // leftover space. These views will get measured again down below if
            // there is any leftover space.
            if (isExactly) {
                mTotalLength += lp.leftMargin + lp.rightMargin;
            } else {
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength +
                        lp.leftMargin + lp.rightMargin);
            }
            ...
        } else {
            int oldWidth = Integer.MIN_VALUE;
            //子View宽度为0,有weight,LayoutParams为WRAP_CONTENT,
            //转换父view的LayoutParams为WRAP_CONTENT
            if (lp.width == 0 && lp.weight > 0) {
                // widthMode is either UNSPECIFIED or AT_MOST, and this
                // child
                // wanted to stretch to fill available space. Translate that to
                // WRAP_CONTENT so that it does not end up with a width of 0
                oldWidth = 0;
                lp.width = LayoutParams.WRAP_CONTENT;
            }

            // Determine how big this child would like to be. If this or
            // previous children have given a weight, then we allow it to
            // use all available space (and we will shrink things later
            // if needed).
            // 测量子View,调用之前说的measureChildWithMargins()方法
            measureChildBeforeLayout(child, i, widthMeasureSpec,
                    totalWeight == 0 ? mTotalLength : 0,
                    heightMeasureSpec, 0);
            ...
        }
        ...
    }
    ...

    // Add in our padding
    //处理padding
    mTotalLength += mPaddingLeft + mPaddingRight;

    int widthSize = mTotalLength;

    // Check against our minimum width
    //view高度与背景尺寸和mMinWidth的运算结果比较,取最大值
    widthSize = Math.max(widthSize, getSuggestedMinimumWidth());

    // Reconcile our calculated size with the widthMeasureSpec
    // MEASURED_SIZE_MASK = 0x00ffffff,取得测量属性的后30位,即尺寸
    int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
    widthSize = widthSizeAndState & MEASURED_SIZE_MASK;

    // Either expand children with weight to take up available space or
    // shrink them if they extend beyond our current bounds. If we skipped
    // measurement on any children, we need to measure them now.
    int delta = widthSize - mTotalLength;
    if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
        float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

        maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
        maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;
        maxHeight = -1;

        mTotalLength = 0;

        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null || child.getVisibility() == View.GONE) {
                continue;
            }

            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            float childExtra = lp.weight;
            if (childExtra > 0) {
                // Child said it could absorb extra space -- give him his share
                // 高度 = 子View的weight*剩余高度/总weight
                int share = (int) (childExtra * delta / weightSum);
                weightSum -= childExtra;
                delta -= share;
                //测量子View
                final int childHeightMeasureSpec = getChildMeasureSpec(
                        heightMeasureSpec,
                        mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
                        lp.height);

                // TODO: Use a field like lp.isMeasured to figure out if this
                // child has been previously measured
                if ((lp.width != 0) || (widthMode != MeasureSpec.EXACTLY)) {
                    // child was measured once already above ... base new measurement
                    // on stored values
                    int childWidth = child.getMeasuredWidth() + share;
                    if (childWidth < 0) {
                        childWidth = 0;
                    }

                    child.measure(
                        MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                        childHeightMeasureSpec);
                } else {
                    // child was skipped in the loop above. Measure for this first time here
                    child.measure(MeasureSpec.makeMeasureSpec(
                            share > 0 ? share : 0, MeasureSpec.EXACTLY),
                            childHeightMeasureSpec);
                }
        ...
    }
}

上述代码,在关键部分增加了一些注释,这里再简单说一下,遍历子View,计算下当前的比重,之后调用measureChildBeforeLayout方法,这个方法将会调用我们之前说的measureChildWithMargins()方法,来完成对子View的测量。接下来用view当前宽度与背景宽度和mMinWidth的运算结果比较,取最大值;再使用resolveSizeAndState(自定义View——雷达图(蜘蛛网图))方法获取测量属性,之后与MEASURED_SIZE_MASK按位与获取view的宽度值。然后再按照weight的属性,对view的剩余宽度进行分配,之后调用getChildMeasureSpec方法进行测量值获取。最后依旧使用child.measure方法,继续对子View进行测量。

现在测量流程到了LinearLayout的子View,我们这里假设是一个View。自然也是调用View的measure方法,之后调用onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

上述代码就是默认的View测量方法,其中setMeasuredDimension将会设置View的测量值,这里需要关注的是getDefaultSize方法,这在我之前的文章自定义View——雷达图(蜘蛛网图),已经进行了分析。但是getDefaultSize方法一般是无法满足我们对LayoutParams = wrap_content情况下的测量要求的,需要我们自己进行一定的修改。

现在已经完成了整个View的测量过程,在整个测量的过程中,我们不断的通过child.measure对子View进行测量,而测量值的获取主要根据View的MeasureSpec、padding,子View的size、LayoutParams、margin以及View的自身特性(比如weight)等属性来完成,这也是我们自己在自定义View时,编写onMeaure方法的主要方式。

三、布局流程

接着第一节的结尾,Layout流程的分析从ViewRootImpl的performLayout开始

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;

    final View host = mView;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        ...
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

布局流程中View的组成和测量流程中一样(DecorView→LinearLayout→View),上述代码,调用了DecorView的layout方法(即View的layout方法),并传入了参数。left、top传入0,rigth传入host.getMeasuredWidth(),bottom传入host.getMeasuredHeight()。接着来看看layout方法

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    // 这里会判断要不要invalidate
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    // 验证PFLAG_LAYOUT_REQUIRED标记
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 需要自定义的确定子布局方法
        onLayout(changed, l, t, r, b);
        // 去除measure时增加的标记
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList listenersCopy =
                    (ArrayList)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);
            }
        }
    }
    // 去除在requestLayout中增加的标记
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

上述代码在主要部分都进行了注释,这里主要看下setFrame方法,使用这个方法传入l, t, r, b,用于确定view在父View中的位置。setOpticalFrame方法最终也会调用setFrame,我们来看下setFrame方法

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        // Invalidate our old position
        invalidate(sizeChanged);
        ...
    }
    return changed;
}

从上述代码可以看出,在setFrame中我们将会判断新旧的位置参数,如果有一个不相等,则会发起invalidate请求,进行View重绘。看到这里也就明白,为什么在requestLayout之后一般都会进行View重绘了。
现在继续来看上面的DecorView#layout方法,在判断是否需要invalidate之后,将会进行PFLAG_LAYOUT_REQUIRED标记的验证,之后运行DecorView的onLayout方法,它会对超出的View会进行平移,这个只是提一下,主要关注的是其继承的FrameLayout的onLayout方法

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom,
                              boolean forceLeftGravity) {
    final int count = getChildCount();

    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
            // 根据不同属性确定子View的位置
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }

            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

上述代码中,主要是遍历所有子View,根据不同的居中情况(如果设置了居中属性的话),重新确定子View的left与top布局,之后根据这些位置以及View测量的宽高,确定right、bottom的位置,最后传递给子View的layout方法。这里的子View按照我们之前在measure中的流程,就是LinearLayout,我们来看看他的onLayout方法

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

LinearLayout的onLyaout方法和onMeasure的逻辑一样,根据LinearLayout属性来选择布局方法,我们也和上次一样选取layoutHorizontal方法看看,这里截取了其中的主要代码

void layoutHorizontal(int left, int top, int right, int bottom) {
    ...
    final int count = getVirtualChildCount();
    ...
    for (int i = 0; i < count; i++) {
        int childIndex = start + dir * i;
        final View child = getVirtualChildAt(childIndex);

        if (child == null) {
            childLeft += measureNullChild(childIndex);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            int childBaseline = -1;

            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) {
                childBaseline = child.getBaseline();
            }

            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }

            switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
                case Gravity.TOP:
                    childTop = paddingTop + lp.topMargin;
                    if (childBaseline != -1) {
                        childTop += maxAscent[INDEX_TOP] - childBaseline;
                    }
                    break;

                case Gravity.CENTER_VERTICAL:
                    // Removed support for baseline alignment when layout_gravity or
                    // gravity == center_vertical. See bug #1038483.
                    // Keep the code around if we need to re-enable this feature
                    // if (childBaseline != -1) {
                    //     // Align baselines vertically only if the child is smaller than us
                    //     if (childSpace - childHeight > 0) {
                    //         childTop = paddingTop + (childSpace / 2) - childBaseline;
                    //     } else {
                    //         childTop = paddingTop + (childSpace - childHeight) / 2;
                    //     }
                    // } else {
                    childTop = paddingTop + ((childSpace - childHeight) / 2)
                            + lp.topMargin - lp.bottomMargin;
                    break;

                case Gravity.BOTTOM:
                    childTop = childBottom - childHeight - lp.bottomMargin;
                    if (childBaseline != -1) {
                        int descent = child.getMeasuredHeight() - childBaseline;
                        childTop -= (maxDescent[INDEX_BOTTOM] - descent);
                    }
                    break;
                default:
                    childTop = paddingTop;
                    break;
            }

            if (hasDividerBeforeChildAt(childIndex)) {
                childLeft += mDividerWidth;
            }

            childLeft += lp.leftMargin;
            // 确定子View布局
            setChildFrame(child, childLeft + getLocationOffset(child), childTop,
                    childWidth, childHeight);
            // 不断增大的childLeft
            childLeft += childWidth + lp.rightMargin +
                    getNextLocationOffset(child);

            i += getChildrenSkipCount(child, childIndex);
        }
    }
}

在上述代码中,会遍历所有的子View,并根据它的居中属性,对childTop进行调整。调用setChildFrame方法来确定子View的布局,如果你跟踪一下,你会发现,它其实调用了child.layout方法,同时每次调用之后,不断增大当前的childLeft,以使下一次的布局不断平移。

按照measure的流程,接下来应该是View的layout流程,View#layout方法,之后又跳转到View的onLayout方法,这个方法只是一个空的实现,一般情况下我们也不需要重载该方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

现在我们已经完成了layout方法的分析。

requestLayout

四、总结

本文分析了View的requestLayout流程,并以DecorView和LinearLayout为例,对View的测量流程、布局流程进行了分析。如果在阅读过程中,有任何疑问与问题,欢迎与我联系。

博客:www.idtkm.com
GitHub:github.com/Idtk
微博:weibo.com/Idtk
邮箱:IdtkMa@gmail.com