View的工作流程-measure過程

前言

上一篇文章深入理解MeasureSpec我們分析了View的測量規(guī)格,根據(jù)MeasureSpec才能測量出View的大小,下面我們詳細分析一下View的測量過程-measure過程,measure過程主要是測量出View的寬/高,其實View的測量分為兩種情況,一是直接繼承View的,通過OnMeasure()過程測量自己就可以了,還有一種是繼承自ViewGroup的,除了測量自身外,還要遍歷去測量所有子View,子View在依次執(zhí)行測量過程。

View的measure過程

View的measure過程是從ViewRootImplperformMeasure()開始的,performMeasure() 里面會調用Viewmeasure()方法,而Viewmeasure()是用final修飾的,意味著子類不能重寫該方法,但是在Viewmeasure()方法里面會去調用我們熟悉的onMeasure()方法,也就是開始了View的測量過程,下面我們看下ViewonMeasure()方法

View#onMeasure()

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

我們發(fā)現(xiàn)這里面邏輯很簡潔,就是調用setMeasuredDimension()方法,也就說當我們自定義一個View重寫onMeasure()時,必須調用setMeasuredDimension()方法,這個方法會存儲View的測量值,我們看下參數(shù)里的getDefaultSize()方法

View#getDefaultSize()

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

這里邏輯也很好理解,我們只需要看MeasureSpec.AT_MOSTMeasureSpec.EXACTLY這兩種情況,getDefaultSize()返回的大小就是測量后的大小,對于MeasureSpec.UNSPECIFIED這種模式,一般用于系統(tǒng)內部的測量過程,View的大小是系統(tǒng)的建議值,也就是getDefaultSize()的第一個參數(shù)getSuggestedMinimumHeight()的返回值,寬高同理

View#getSuggestedMinimumWidth()

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

我們發(fā)現(xiàn)就是根據(jù)View是否設置背景進行判斷的,如果沒設置背景就返回mMinWidth,設置背景看就返回mMinWidth和背景寬度的最大值,mMinWidth對應于android:minWidth,如果不指定默認為0;我們再看下mBackground.getMinimumWidth()的含義,在View的全局變量里我們看到private Drawable mBackground;背景就是一個Drawable,再看下getMinimumWidth()

Drawable#getMinimumWidth()

     * @return The minimum width suggested by this Drawable. If this Drawable
     *         doesn't have a suggested minimum width, 0 is returned.
     */
    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

我們看到注釋沒如果沒設置背景就返回0,否則就返回背景的原始寬度,這個原始寬度又是什么呢,例如在xml中定義shape填充這種純色,原始寬度就為0,如果設置BitmapDrawable為背景,原始寬度就不為0.通過以上可以看出getSuggestedMinimumWidth()返回的就是View在MeasureSpec.UNSPECIFIED這種模式下測量的大小。以上就是我們分析普通View的onMeasure過程。

ViewGroup的measure過程

ViewGroup是繼承與View的,它是一個抽象類沒有重寫onMeasure方法,但是有一個measureChildren()方法,通過名字我們也能猜到就是測量子View的方法,下面我們詳細看下

ViewGroup#measureChildren()

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

這里就是通過遍歷所有的子View并且不是GONE的情況下去執(zhí)行measureChild()方法,這也很好的解釋了為什么我在將某個View設置GONE后,不占用空間的原因,因為根本就沒有去測量,我們再看下measureChild()方法

ViewGroup#measureChild()

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

這里邏輯也很簡單,首先取出子ViewLayoutParams,將父控件的MeasureSpecLayoutParams作為參數(shù)傳到getChildMeasureSpec()方法中來創(chuàng)建子ViewMeasureSpecgetChildMeasureSpec()這個方法在上一篇深入理解MeasureSpec中已經(jīng)詳細的介紹,請自行查看!子View調用 measure方法,里面會執(zhí)行View的onMeasure()方法,下面我們看下FrameLayout的測量過程。

FrameLayout#onMeasure()

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    //判斷當前布局的寬高是否為EXACTLY模式
    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) {
            //對每一個子View進行測量
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //尋找子View中寬高的最大者,因為如果FrameLayout是wrap_content屬性
            //那么它的大小取決于子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());
            //如果FrameLayout非EXACTLY模式,添加到mMatchParentChildren中
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

   ...

    //保存測量結果
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    //子View中設置為match_parent的個數(shù)
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            //對FrameLayout的寬度規(guī)格設置,因為這會影響子View的測量
            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);
            }
            ...
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

首先,F(xiàn)rameLayout根據(jù)它的MeasureSpec來對每一個不為GONE的子View進行測量,即調用measureChildWithMargin()方法,measureChildWithMargin()這個方法和measureChild()類似,多計算了邊距,再對每一個子View進行測量,之后會尋找其中最大的寬高,那么FrameLayout的測量寬高會受到這個子View的最大寬高的影響(wrap_content模式),接著調用setMeasureDimension方法,把FrameLayout的測量寬高保存。最后則是特殊情況的處理,即當FrameLayout為wrap_content屬性時,如果其子View是match_parent屬性的話,則要重新設置FrameLayout的測量規(guī)格,然后重新對該部分View測量。

總結

上面我們已經(jīng)分析了ViewMeasure過程,測量控件大小是父控件發(fā)起的
父控件要測量子控件大小,需要重寫onMeasure()方法,然后調用measureChildren()或者measureChildWithMargins()方法
測量控件的步驟:
父控件onMeasure->measureChildren/measureChildWithMargin->getChildMeasureSpec->
子控件的measure->onMeasure->setMeasureDimension->
父控件onMeasure結束調用setMeasureDimension最后保存自己的大小.

推薦

深入理解MeasureSpec

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容