View的工作原理——理解MeasureSpec

爲了更好地理解View地測量過程,我們還需要MeasureSpec。從名字上來看,MeasureSpec看起來像“測量規格”或者“測量說明書”,不管怎麼翻譯,它看起來都好像是或多或少地決定了View的測量過程。通過源碼可以發現,MeasureSpec的確參與了View的measure過程。那麼,MeasureSpec是幹什麼的呢?確切來說,MeasureSpec在很大程度上決定了一個View的尺寸規格,之所以說是很大程度上是因爲這個過程還受父容器的影響,因爲父容器影響View的MeasureSpec的創建過程。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後在根據這個MeasureSpec來測量出View的寬/高。上面提到過,這裏的寬/高是測量寬/高,不一定等於View的最終寬/高。MeasureSpec看起來有點複雜,其實它的實現過程很簡單的,下面會詳細地分析MeasureSpec。

1.MeasureSpec

MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代碼SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下地規格地大小。下面先看一下MeasureSpec內部地一些常量地定義,通過下面地代碼,應該不難理解MeasureSpec的工作過程。


public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * 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;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
         * will automatically get a size of 0. Older apps expect this.
         *
         * @hide internal use only for compatibility with system widgets and older apps
         */
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
}

MeasureSpec通過SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,爲了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一個int值,一組SpecMode和SpecSize可以打包位一個MeasureSpec,而一個MeasureSpec可以通過解包的形式來得出其原始的SpecMode和SpecSize,需要注意的是這裏提到的MeasureSpec是指MeasureSpec所代表的int值,而非MeasureSpec本身。

SpecMode有三類,每一類都表示特殊的含義,如下所示。
UNSPECIFIED

父容器不對View有任何限制,要多大給多大,這種情況一般用於系統內部,表示一種測量狀態。

EXACTLY

父容器以及檢測出View所需要的大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體數值這兩種模式。

AT_MOST
父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼要看不同View的具體實現。它對應於LayoutParams中的wrap_content。

2.MeasureSpec和LayoutParams的對應關係

上面提到,系統內部是通過MeasureSpec來進行View的測量,但是正常情況下我們使用View指定MeasureSpec,儘管如此,但是我們可以給View設置LayoutParams。在View測量的時候,系統會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬高。需要注意的是,MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬高。另外,對於頂級View(即DecorView)和普通View來說,MeasureSpec的轉換過程略有不同。對於DecorView,其Measure由窗口的尺寸和其自身的LayoutParams來共同決定;對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure中就可以確定View測量的寬/高。
對於DecorView來說,在ViewRootImpl中的measureHierarchy方法中由如下一段代碼,它展示了DecorView的MeasureSpec的創建過程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸:

            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着再看一下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;
    }

通過上述代碼,Decor的MeasureSpec的產生過程就很明確了,具體來說遵守如下規則,根據它的LayoutParams中的寬高的參數來劃分。

  • LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小
  • 固定大小(比如100dp):精確模式,大小位LayoutParams中指定的大小。
對於普通View來說,這裏我們指佈局中的View,View的measure過程由ViewGroup傳遞而來,先看一下ViewGroup的measureChildWithMargins方法:

    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);
    }

上述方法回對子元素進行measure,在調用子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec。從代碼來看,很顯然,子元素的Measure的創建與父容器的Measure和子元素本身的LayoutParams有關,此外還和View的margin以及padding有關,具體情況可以看下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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上述方法不難理解,它的主要作用是根據父容器的MeasureSpec同時結合View本身的LayoutParams來確定子元素的MeasureSpec,參數中的padding是指父容器中已佔用的空間大小,因此子元素可用的大小位父容器的尺寸減去padding,具體代碼如下所示。
        int specSize = MeasureSpec.getSize(spec);
        int size = Math.max(0, specSize - padding);

getChildMeasureSpec清楚展示了普通View的MeasureSpec的創建規則,爲了更加清晰地理解getChildMeasureSpec的邏輯,這裏提供一個表,表中對getChildMeasureSpec的工作原理進行了梳理,請看下錶。注意,表中的parentSize是指父容器中目前可使用的大小。

針對表,這裏再做一下說明。簽名已經提到,對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,那麼針對不同的父容器和View本身不同的LayoutParams,View就可以有多種MeasureSpec。這裏簡單說下,當View採用固定寬高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是精確模式且其大小遵循LayoutParams中的大小。當View的寬高是match_parent時,如果父容器的模式是精準模式,那麼View也是最大模式並且其大小不過超過父容器的剩餘空間;如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩餘空間。當View的寬高是wrap_content時,不管父容器的模式時精準還是最大化,View的模式總是最大化並且大小不能超過父容器的剩餘空間。可能讀者會發現,在我們的分析中漏掉了UNSPEIFIED模式,那是因爲這個模式主要用於系統內部多次Measure的情形,一般來說,我們也不需要關注此模式。
通過上表可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地確定出子元素地MeasureSpec了,有了MeasureSpec就可以進一步確定出子元素測量後的大小了。需要說明的是,表中並非是什麼經驗總結,它只是getChildMeasureSpec這個方法以表格的方式呈現出來而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章