Android View和ViewGroup的measure過程


本篇文章主要分析View和ViewGroup的measure過程儿礼, 由于ViewGroup還可以包含子元素瑞信, 所以相對于View來說會有幾個對子View measure的方法夹孔, 下面講分別分析。

概述

View和ViewGroup的工作流程主要指measure序调、layout、draw這三大流程或听, 即測量妥粟、布局和繪制恬试。measure流程用來測量View和ViewGroup所占矩形區(qū)域的大小窝趣, 即測量寬高(最終寬高可能會因為layout過程而改變)。layout流程確定View和ViewGroup的最終寬高和矩形區(qū)域在父布局中的位置坐標(biāo)(父布局再一層層往上训柴, 最終會是確定了在屏幕中的位置坐標(biāo))哑舒。draw流程則是講View和ViewGroup繪制在屏幕上(底層調(diào)用SurfaceFlinger進(jìn)行繪制, 這里不進(jìn)行分析)幻馁。

View 的measure過程

首先看一下android 7.0 中View.java中的measure()方法如下洗鸵,被定義成了final的,View的子類將不能重寫此方法仗嗦。方法文檔寫到:調(diào)用該方法用來查明一個view應(yīng)該是多大膘滨,該view所在的父容器提供寬高參數(shù)的約束信息。實(shí)際的measure工作在onMeasure方法中進(jìn)行稀拐,因此只有onMeasure方法可以被子類重寫吏祸。

    /**
     * <p>
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * </p>
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // 判斷當(dāng)前view的LayoutMode是否為opticalbounds
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) { // 判斷當(dāng)前view的父容器的LayoutMode是否為opticalbounds
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        // 根據(jù)我們傳入的widthMeasureSpec和heightMeasureSpec計算key值,我們在mMeasureCache中存儲我們view的信息
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        // 如果mMeasureCache為null钩蚊,則進(jìn)行new一個對象
        if (mMeasureCache == null)  mMeasureCache = new LongSparseLongArray(2);
        // mOldWidthMeasureSpec和mOldHeightMeasureSpec分別表示上次對View進(jìn)行量算時的widthMeasureSpec和heightMeasureSpec
        // 執(zhí)行View的measure方法時,View總是先檢查一下是不是真的有必要費(fèi)很大力氣去做真正的量算工作
        // mPrivateFlags是一個Int類型的值蹈矮,其記錄了View的各種狀態(tài)位
        // 如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT砰逻,
        // 那么表示當(dāng)前View需要強(qiáng)制進(jìn)行l(wèi)ayout(比如執(zhí)行了View的forceLayout方法),所以這種情況下要嘗試進(jìn)行量算
        // 如果新傳入的widthMeasureSpec/heightMeasureSpec與上次量算時的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等泛鸟,
        // 那么也就是說該View的父ViewGroup對該View的尺寸的限制情況有變化蝠咆,這種情況下要嘗試進(jìn)行量算
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            // 通過運(yùn)算,重置mPrivateFlags值,即View的測量狀態(tài)
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            // 解決布局中的Rtl問題
            resolveRtlPropertiesIfNeeded();
            // 判斷當(dāng)前View是否是強(qiáng)制進(jìn)行測量刚操,如果是則將cacheIndex=-1闸翅,反之從mMeasureCache中獲取
            // 對應(yīng)的index,即從緩存中讀取存儲的大小菊霜。
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
           // 根據(jù)cacheIndex的大小判斷是否需要重新測量坚冀,或者根據(jù)布爾變量sIgnoreMeasureCache進(jìn)行判斷。
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // 重新測量鉴逞,則調(diào)用我們重寫的onMeasure()方法進(jìn)行測量记某,然后重置View的狀態(tài)
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                // 通過我們計算的cacheIndex值,從緩存中讀取我們的測量值构捡。
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                // 通過setMeasuredDimension()方法設(shè)置我們的測量值液南,然后重置View的狀態(tài)
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            // 如果View的狀態(tài)沒有改變,則會拋出異常"我們沒有調(diào)用”setMeasuredDimension()"方法勾徽,一般出現(xiàn)在我們重寫onMeasure方法滑凉,
            // 但是沒有調(diào)用setMeasuredDimension方法導(dǎo)致的。
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        // 將最新的widthMeasureSpec和heightMeasureSpec進(jìn)行存儲到mMeasureCache
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }   

上面的measure方法大致的描述了測量的流程喘帚。

  • 1 首先判斷當(dāng)前View的layoutMode模式畅姊,通過調(diào)用isLayoutModeOptical方法進(jìn)行判斷。這個方法就是判斷view是否為ViewGroup類型啥辨,然后判斷l(xiāng)ayoutMode設(shè)定是否為opticalBounds涡匀。如果是,則對傳入的widthMeasureSpec溉知、heightMeasureSpec進(jìn)行重新計算封裝陨瘩。
  • 2 判斷當(dāng)前view是否強(qiáng)制重新計算,或者傳入進(jìn)來的MeasureSpec是否和上次不同级乍。這兩種情況滿足一種則進(jìn)行測量運(yùn)算舌劳。
  • 3 如果為強(qiáng)制測量或者忽略緩存,則調(diào)用onMeasure()方法進(jìn)行測量玫荣,反之甚淡,從mMeasureCache緩存中讀取上次的測量數(shù)據(jù)。
  • 4 將最新的widthMeasureSpec和heightMeasureSpec進(jìn)行存儲到mMeasureCache捅厂。

上面這段代碼主要涉及到3個主要的地方:

  • 1 android:layoutMode屬性的判斷贯卦,如果是opticalBounds則需要重新計算MeasureSpec。
  • 2 MeasureSpec的概念焙贷。
  • 3 onMeasure方法撵割。

ViewGroup的android:layoutMode屬性

android:layoutMode屬性我們很少用的到,設(shè)置layoutMode屬性有兩個常量值辙芍,默認(rèn)是clipBounds啡彬,clipBounds 表示了getLeft()羹与、getRight()等返回的原始值組成的邊界;opticalBounds描述了視覺上的邊界庶灿,視覺邊界位于Clip bounds中纵搁,Clip bounds會更大,因為用來顯示一些其他效果比如陰影和閃光等往踢。比如Button腾誉,它的clipBounds比opticalBounds大一圈。


button.png
    /**
     * This constant is a {@link #setLayoutMode(int) layoutMode}.
     * Clip bounds are the raw values of {@link #getLeft() left}, {@link #getTop() top},
     * {@link #getRight() right} and {@link #getBottom() bottom}.
     */
    public static final int LAYOUT_MODE_CLIP_BOUNDS = 0;

    /**
     * This constant is a {@link #setLayoutMode(int) layoutMode}.
     * Optical bounds describe where a widget appears to be. They sit inside the clip
     * bounds which need to cover a larger area to allow other effects,
     * such as shadows and glows, to be drawn.
     */
    public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1;

官方介紹

layoutMode.png

View.java中的isLayoutModeOptical()方法

    /**
     * Return true if o is a ViewGroup that is laying out using optical bounds.
     * @hide
     */
    public static boolean isLayoutModeOptical(Object o) {
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }

ViewGroup.java中的isLayoutModeOptical() 方法

    /** Return true if this ViewGroup is laying out using optical bounds. */
    boolean isLayoutModeOptical() {
        return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
    }

MeasureSpec的概念

measure()方法中傳的是MeasureSpec變量菲语,在View測量的時候妄辩,系統(tǒng)會將該View的LayoutParams在父容器的約束(父容器的MeasureSpec)下轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后再根據(jù)這個MeasureSpec來確定View的測量寬高山上。它是View的一個內(nèi)部類眼耀,代表了一個32位int值,高2位代表SpecMode佩憾,即測量模式哮伟。低30位代表SpecSize,即在該SpecMode下面的規(guī)格大小妄帘。

SpecMode有三種模式:

  • 1 UNSPECIFIED(未指定)楞黄,父容器不對子元素施加任何束縛,子元素可以得到任意想要的大小抡驼,一般用于系統(tǒng)內(nèi)部鬼廓,標(biāo)識一種測量的狀態(tài)。
  • 2 EXACTLY(精準(zhǔn))致盟,父容器已經(jīng)檢測出子元素所需要的確切大小碎税,這個時候子元素的最終大小就是SpecSize所指定的值。它對應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種模式馏锡。
  • 3 AT_MOST(至多)雷蹂,父容器指定了一個可用大小,即SpecSize杯道,子元素的大小不能大于這個值匪煌,具體是什么要看不同子元素的具體實(shí)現(xiàn),它對應(yīng)于LayoutParams中的wrap_content党巾。
    MeasureSpec的部分源碼:
/**
  * A MeasureSpec encapsulates the layout requirements passed from parent to child.
  * Each MeasureSpec represents a requirement for either the width or the height.
  * A MeasureSpec is comprised of a size and a mode.
  */
  public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    /**
     * 根據(jù)specSize和specMode打包成MeasureSpec
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    /**
     * 根據(jù)MeasureSpec拆分出specMode
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    /**
     * 根據(jù)MeasureSpec拆分出specSize
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
 }

onMeasure方法

onMeasure方法測量View和它的內(nèi)容來決定測量寬高萎庭,該方法被measure方法調(diào)用,它應(yīng)該被子類重寫用來提供精準(zhǔn)高效的測量齿拂。

/**
 * <p>
 * Measure the view and its content to determine the measured width and the
 * measured height. This method is invoked by {@link #measure(int, int)} and
 * should be overridden by subclasses to provide accurate and efficient
 * measurement of their contents.
 * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent.
 *                         The requirements are encoded with
 *                         {@link android.view.View.MeasureSpec}.
 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension()方法用來存儲測量后的寬高驳规,調(diào)用此方法完成后,測量過程就已經(jīng)結(jié)束了创肥,這之后可以拿到測量后的寬高值。

    /**
     * <p>This method must be called by {@link #onMeasure(int, int)} to store the
     * measured width and measured height. Failing to do so will trigger an
     * exception at measurement time.</p>
     *
     * @param measuredWidth The measured width of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     * @param measuredHeight The measured height of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     */
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

在onMeasure方法調(diào)用setMeasuredDimension()時, setMeasuredDimension()中傳入的是getDefaultSize()的返回值叹侄,看一下getDefaultSize()方法:

    /**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

可以看出getDefaultSize()方法的邏輯很簡單巩搏,我們只需要看AT_MOST和EXACTLY兩種情況,getDefaultSize()返回的就是measureSpec的specSize 趾代,而這傳入的measureSpec是在父容器中由父容器的measureSpec和當(dāng)前子元素的LayoutParams已經(jīng)共同確定的贯底,所以由measureSpec拆分出來的specSize 就是子元素測量后的大小。針對UNSPECIFIED的情況撒强,一般用于系統(tǒng)內(nèi)部的測量過程禽捆,下面以分析getSuggestedMinimumWidth()為例分析該函數(shù)的返回值意義:

    /**
     * Returns the suggested minimum width that the view should use. This
     * returns the maximum of the view's minimum width)
     * and the background's minimum width
     *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned width is within the requirements of the parent.
     *
     * @return The suggested minimum width of the view.
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

可以看出,如果View沒有設(shè)置背景飘哨,那么View的寬度即為mMinWidth 胚想,而mMinWidth 對應(yīng)于android:mMinWidth 這個屬性的值,如果沒有設(shè)置該屬性則mMinWidth 默認(rèn)為0芽隆;如果View指定了背景浊服。則View的寬度為max(mMinWidth, mBackground.getMinimumWidth()),那么mBackground.getMinimumWidth()是什么呢胚吁?看一下Drawable的getMinimumWidth()方法:

    /**
     * Returns the minimum width suggested by this Drawable. If a View uses this
     * Drawable as a background, it is suggested that the View use at least this
     * value for its width. (There will be some scenarios where this will not be
     * possible.) This value should INCLUDE any padding.
     *
     * @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;
    }

可以看出牙躺,getMinimumWidth()返回的就是Drawable的原始寬度,如果沒有原始寬度就返回0腕扶∧蹩剑總結(jié)一下getSuggestedMinimumWidth()的主要工作:如果View沒有設(shè)置背景就返回android:mMinWidth 這個屬性的值,可以人為設(shè)置該值半抱,默認(rèn)為0脓恕;如果View設(shè)置了背景,則返回mMinWidth和背景最小寬度兩者中的最大值代虾。getSuggestedMinimumWidth()的返回值就是View在UNSPECIFIED情況下的測量寬进肯。

直接繼承自View的自定義控件如果在xml布局中設(shè)置了wrap_content屬性,則會實(shí)際表現(xiàn)起來相當(dāng)于match_parent棉磨。從getDefaultSize()方法可以看出江掩,當(dāng)View設(shè)置wrap_content屬性時,會是MeasureSpec.AT_MOST模式乘瓤,和match_parent一樣环形,所以需要給改自定義View設(shè)置默認(rèn)的寬高值以表現(xiàn)出wrap_content的效果。TextView針對wrap_content進(jìn)行了特殊處理衙傀,在TextView的onMeasure方法中傳入的MeasureSpec中的specSize重新計算了一遍抬吟,并沒有使用從父容器傳來的specSize(該specSize指父容器所能提供的最大空間,和match_parent一樣的效果)统抬。

說到最后火本,可能會有疑問危队,頂層View--DecorView的MeasureSpec是如何獲得的呢?因為子元素的測量需要父容器的MeasureSpec和自身的LayoutParams共同決定(這點(diǎn)在ViewGroup的測量過程中能看出钙畔,接下來分析)茫陆,所以頂層父容器DecorView的MeasureSpec必須最先獲取,然后才能開始遍歷子元素的測量工作擎析。

分析Activity的setContentView方法可知簿盅,DecorView在PhoneWindow中被創(chuàng)建,在Activity的onResume方法回調(diào)的時候繼而調(diào)用Activity的makeVisible()方法揍魂,Activity對應(yīng)的PhoneWindow對象才被WindowManager添加桨醋。此后開始View的三大流程。

// ActivityThread.java
final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ...
    r = performResumeActivity(token, clearHide, reason);
    if (r != null) {
        final Activity a = r.activity;
        ...
        
        if (r.activity.mVisibleFromClient) {
            r.activity.makeVisible();
        }
    }
    
}

// Activity.java的makeVisible()方法:
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

之后:
WindowManager.addView()->WindowManagerImpl.addView()->WindowManagerGlobal.addView()->創(chuàng)建ViewRootImpl對象->ViewRootImpl.setView(DecorView對象)->ViewRootImpl.requestLayout()->ViewRootImpl.performTraversals()->然后觸發(fā)performMeasure()现斋、performLayout()喜最、performDraw()。

WindowManagerGlobal.addView()核心代碼:
...
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ...
    root.setView(view, wparams, panelParentView);
    ...
}

先說結(jié)論:DecorView的MeasureSpec是由窗口的尺寸(PhoneWindow)和其自身的LayoutParams共同決定的步责,(對于普通View返顺,MeasureSpec是父容器的MeasureSpec和其自身的LayoutParams共同決定),MeasureSpec一旦確定蔓肯,onMeasure中就可以確定View的測量寬高遂鹊。

對于DecorView,在ViewRootImpl的measureHierarchy方法計算出DecorView的MeasureSpec值蔗包,如下:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    ...
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

其中desiredWindowWidth秉扑、desiredWindowHeight分別對應(yīng)著Window的寬高(屏幕的尺寸)。下面看一下getRootMeasureSpec()方法:

    /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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;
    }
  • 1 WindowManager.LayoutParams.MATCH_PARENT:精準(zhǔn)模式调限,SpecSize就是窗口的大小舟陆。
  • 2 WindowManager.LayoutParams.WRAP_CONTENT:最大模式,大小不定耻矮,但是不能超過窗口的大小秦躯。
  • 3 固定大小:精準(zhǔn)模式裆装,大小為LayoutParams中指定的值踱承。

頂層DecorView的MeasureSpec確定后,開始遍歷遞歸子元素進(jìn)行測量流程哨免,然后如果遇到了View就是上面分析的測量流程茎活,如果遇到的是ViewGroup則接下來進(jìn)行分析。


ViewGroup 的measure過程

ViewGroup繼承自View琢唾,View中的measure方法由于final不能被重新载荔,onMeasure也沒有重寫。ViewGroup 的measure情況分為兩種采桃,一個是measure自己懒熙,另一個是measure子元素丘损。但是ViewGroup并沒有定義具體的measure自己的過程,其測量過程的onMeasure需要由各個具體的子類去實(shí)現(xiàn)工扎,比如LinearLayout号俐、RelativeLayout,因為不同的ViewGroup子類有不同的布局特性定庵。

ViewGroup measure子元素過程有measureChildren()方法如下:

    /**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    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);
            }
        }
    }

其內(nèi)部通過循環(huán)遍歷所有的子元素,對可見的子元素再調(diào)用measureChild()方法:

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }

measureChildWithMargins()方法也類似踪危,在調(diào)用getChildMeasureSpec()方法獲取子元素的MeasureSpec中增加了子元素的Margin值蔬浙。measureChild()函數(shù)內(nèi)部清晰的表達(dá)了子元素 MeasureSpec的產(chǎn)生過程:先得到子元素的LayoutParams,然后調(diào)用getChildMeasureSpec()方法將父容器的parentMeasureSpec與padding值贞远、子元素的LayoutParams一起合成了子元素的MeasureSpec畴博。getChildMeasureSpec()方法如下:

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面代碼的主要邏輯是:如果子元素的LayoutParams中寬高是具體精準(zhǔn)數(shù)值時,不管父容器的 MeasureSpec是什么蓝仲,子元素的MeasureSpec都是精準(zhǔn)模式切大小遵循LayoutParams中的具體數(shù)值寬高俱病;如果子元素的LayoutParams中寬高是match_parent,當(dāng)父容器是精準(zhǔn)模式時袱结,子元素也是精準(zhǔn)模式并且大小不會超過父容器剩余空間大小亮隙,當(dāng)父容器是最大模式時,子元素也是最大模式并且其大小不會超過父容器剩余空間垢夹;如果子元素的LayoutParams中寬高是wrap_content時溢吻,不管父容器是精準(zhǔn)模式還是最大化,子元素的模式總是最大化且不能超過父容器的剩余空間果元。


以LinearLayout分析ViewGroup的measure自己過程

LinearLayout的onMeasure()方法:

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

看得到有垂直測量和水平測量兩種促王,下面分析 measureVertical()方法的大致邏輯:

// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
    if (child == null) {
        mTotalLength += measureNullChild(i);
        continue;
    }

    if (child.getVisibility() == View.GONE) {
       i += getChildrenSkipCount(child, i);
       continue;
    }

    if (hasDividerBeforeChildAt(i)) {
        mTotalLength += mDividerHeight;
    }

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

    totalWeight += lp.weight;

    ...
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);
    ...
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
    ...

從上面這段代碼可以看出,會遍歷子元素并對每個子元素執(zhí)行measureChildBeforeLayout()方法而晒,這個方法內(nèi)部會調(diào)用子元素的measure()方法蝇狼,然后各個子元素開始一次進(jìn)入measure過程,系統(tǒng)通過mTotalLength來存儲LinearLayout在豎直方向的初步高度倡怎。每測量一個元素迅耘,mTotalLength就會增加,增加的部分包括子元素的高度以及子元素在豎直方向的margin等诈胜,當(dāng)子元素測量完畢后豹障,LinearLayout會測量自己的大小:

// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);

如果LinearLayout在布局中指定的高度是wrap_content焦匈,那么LinearLayout的高度是等有所子元素測量完畢后子元素所占用的高度總和血公,但是仍然不能超過LinearLayout的父容器的剩余空間,還需要考慮豎直方向的pading缓熟。resolveSizeAndState()函數(shù)主要是完成最后的測量累魔。


獲取View寬高的時機(jī)

  • 1 Activity/View的onWindowFocusChanged.View已經(jīng)初始化完畢摔笤,寬高準(zhǔn)備好了,onWindowFocusChanged會被調(diào)用多次垦写,當(dāng)Activity的窗口得到焦點(diǎn)和失去焦點(diǎn)的時候均會被調(diào)用吕世,對應(yīng)Activity的onResume和onPause方法。
    @Override
    public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus)
        {
            System.out.println("onWindowFocusChanged width=" + mTextView.getWidth() + " height=" + mTextView.getHeight());
        }
    }
  • 2 View.post(runnable).通過post可以將一個runnable投遞到消息隊列的尾部梯投,然后等待Looper調(diào)用此runnable的時候命辖,View已經(jīng)初始化好了。
view.post(new Runnable(){
     @Override
     public void run() {
          int width = view.getMeasuredWidth();          
          int height = view.getMeasuredHeight();
      }
});
  • 3 ViewTreeObserver. 使用ViewTreeObserver的很多回調(diào)方法可以獲取View的寬高分蓖,比如OnGlobalLayoutListener和OnPreDrawListener尔艇。當(dāng)View樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的View可見性發(fā)生改變時,OnGlobalLayoutListener會被回調(diào)么鹤,伴隨著View樹的狀態(tài)改變等终娃,addOnGlobalLayoutListener方法會被回調(diào)多次。
  // MineFragment中蒸甜,顯示"圈子棠耕,新人必讀"引導(dǎo)頁的PopupWindow中使用
  private void showPopWindow() {
    layoutAbovePopWindow.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {
        changeWindowAlpha(0.7f);
        popupWindow.showAsDropDown(layoutAbovePopWindow, 0, Tools.getPixelByDip(getActivity(), 10));
        popupWindow.update();
        layoutAbovePopWindow.getViewTreeObserver().removeGlobalOnLayoutListener(this);
      }
    });
  }
  // 開戶上傳身份證照片UploadPhotoActivity.java中使用
  private void initUI() {
    getPresenter().initUploadLayout(mCurrentPartnerId);
    mUploadPhotoView.setClearListener(this);
    mUploadPhotoView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
      @Override
      public boolean onPreDraw() {
        int photoLayoutHeight = mUploadPhotoView.getMeasuredHeight();
        if (photoLayoutHeight > 0) {
          mUploadPhotoView.setPhotoLayout(photoLayoutHeight);
        }
        mUploadPhotoView.getViewTreeObserver().removeOnPreDrawListener(this);
        return false;
      }
    });
  }
  • 4 手動對View進(jìn)行measure來得到View的寬高,該方法不常用柠新。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末窍荧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恨憎,更是在濱河造成了極大的恐慌搅荞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件框咙,死亡現(xiàn)場離奇詭異咕痛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)喇嘱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門茉贡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人者铜,你說我怎么就攤上這事腔丧。” “怎么了作烟?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵愉粤,是天一觀的道長。 經(jīng)常有香客問我拿撩,道長衣厘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮影暴,結(jié)果婚禮上错邦,老公的妹妹穿的比我還像新娘。我一直安慰自己型宙,他們只是感情好撬呢,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著妆兑,像睡著了一般魂拦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搁嗓,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天晨另,我揣著相機(jī)與錄音,去河邊找鬼谱姓。 笑死,一個胖子當(dāng)著我的面吹牛刨晴,可吹牛的內(nèi)容都是我干的屉来。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼狈癞,長吁一口氣:“原來是場噩夢啊……” “哼茄靠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝶桶,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤慨绳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后真竖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脐雪,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年恢共,在試婚紗的時候發(fā)現(xiàn)自己被綠了战秋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡讨韭,死狀恐怖脂信,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情透硝,我是刑警寧澤狰闪,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站濒生,受9級特大地震影響埋泵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜罪治,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一秋泄、第九天 我趴在偏房一處隱蔽的房頂上張望琐馆。 院中可真熱鬧,春花似錦恒序、人聲如沸瘦麸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滋饲。三九已至,卻和暖如春喊巍,著一層夾襖步出監(jiān)牢的瞬間屠缭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工崭参, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呵曹,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓何暮,卻偏偏與公主長得像奄喂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子海洼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容