1. 程式人生 > >Android-View寬高測量研究

Android-View寬高測量研究

前言

平時寫自定義控制元件包括之前寫下拉重新整理庫的時候,有時候都需要預先知道View的寬度或者高度,這樣能夠幫助我們很好地實現效果,但是我們都或多或少知道View的度量和繪製是立刻完成的操作,所以當我初始化一個View之後,是無法立刻拿到它的寬度和高度值的,這時候去google一下,網友就會告訴你,用measure()方法呼叫一下,就可以獲取了,於是急急忙忙把程式碼段從其他地方copy來使用,就這樣不知道解決了多少次我的問題。 但是,用了那麼多次,我只是知道它是這麼“救急”,卻不知道它是如何工作的,現在趁著晚上有空,我研究了一下。

View.measure() 方法測量寬高

先來了解View.MeasureSpec

Google官方對其解釋:

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. There are three possible modes:

UNSPECIFIED
The parent has not imposed any
constraint on the child. It can be whatever size it wants. //父佈局不對子View做任何約束,子View的大小能由自己決定。 EXACTLY 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. //父佈局對子View給出精確大小,子View將會被這些邊界限制而不管它自己想要多大。 AT_MOST The child can be as
large as it wants up to the specified size. MeasureSpecs are implemented as ints to reduce object allocation. This class is provided to pack and unpack the <size, mode> tuple into the int. //子View大小一般隨著控制元件的子空間或內容進行變化,此時控制元件尺寸只要不超過父控制元件允許的最大尺寸即可。

我們知道MeasureSpec是Android中父佈局傳遞給子View用來描述其對子View佈局需求的資料型別,也就是說父佈局把它希望子View的大小以及變化的尺度封裝在這個類中。而子View的measure方法拿到這個數值,則會根據這個數值中的資訊對自身進行度量。


進入原始碼檢視:

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

從上面我們得出

MASK = 0x3 << 30 = 0xC0000000,前4位為 1100;
UNSPECTIFIED = 0 << 30 = 0x00000000,前4位為 0000;
EXACTLY = 1 << 30 = 0x40000000, 前4位為 0100;
AT_MOST = 2 << 30 = 0x80000000, 前4位為 1000;

繼續檢視MeasureSpec類的程式碼:

        /**
         * 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(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        /**
         * 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}
         */
        public static int getMode(int measureSpec) {
            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);
        }

measrureSpec 是由size和mode兩個int數值做二進位制運算,mode & MODE_MASK 即取mode的前2位,size & ~MODE_MASK 即取size的後30位,然後這兩部分組成了我們想要的measureSpec的值,這樣做不僅能夠滿足對於描述佈局的需求,還節省的記憶體開支。

measure()方法測量流程

知識鋪墊看完,看到measure()方法的屬性為public,包括layout()方法,以及draw()方法,外部程式碼框架通過呼叫這幾個方法,來實現View的測量,定位,以及繪製。

   /**
     * <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) {

measure()方法中會再呼叫onMeasure()方法

            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

通過在onMeasure方法中對View自身的寬高進行測量,這裡先看View的onMeasure()方法,檢視View類的onMeasure()方法,這裡呼叫setMeasuredDimension()來設定測量的寬度和高度。

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

而測量的高度和寬度則是根據MeasureSpec計算出來的,我們可以看下面的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;
    }

這裡通過解析MeasureSpec獲取specMode(父佈局傳遞進來的度量模式),specSize(父佈局傳遞進來的尺寸數值)

  • 當值為MeasureSpec.UNSPECIFIED,result 直接賦值為View自身的實際尺寸
  • 當值為MeasureSpec.AT_MOST 或者 MeasureSpec.EXACTLY,result 賦值為父佈局傳遞進來的尺寸數值。
  • 看過了View類的onMeasure()如何對自身進行測量,再看ImageView的onMeasure()如何對自身進行測量。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            resolveUri();
            int w;
            int h;
            ...
             w += pleft + pright;
             h += ptop + pbottom;
    
             w = Math.max(w, getSuggestedMinimumWidth());
             h = Math.max(h, getSuggestedMinimumHeight());
    
             widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
             heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
             ...
             setMeasuredDimension(widthSize, heightSize);
       }

    繼續檢視resolveSizeAndState()方法,根據父類傳遞的MeasureSpec測量自身的邏輯

            public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
            final int specMode = MeasureSpec.getMode(measureSpec);
            final int specSize = MeasureSpec.getSize(measureSpec);
            final int result;
            switch (specMode) {
                case MeasureSpec.AT_MOST:
                    if (specSize < size) {
                        result = specSize | MEASURED_STATE_TOO_SMALL;
                    } else {
                        result = size;
                    }
                    break;
                case MeasureSpec.EXACTLY:
                    result = specSize;
                    break;
                case MeasureSpec.UNSPECIFIED:
                default:
                    result = size;
            }
            return result | (childMeasuredState & MEASURED_STATE_MASK);
        }

    這裡通過解析MeasureSpec獲取specMode(父佈局傳遞進來的度量模式),specSize(父佈局傳遞進來的尺寸數值)

  • 當值為MeasureSpec.UNSPECIFIED,result 直接賦值為View自身的實際尺寸
  • 當值為MeasureSpec.AT_MOST,當實際尺寸不超過specSize的時候,result賦值為View自身實際尺寸,當實際尺寸超過specSize的時候,result賦值為specSize.
  • 當值為MeasureSpec.EXACTLY,result 賦值為父佈局傳遞進來的尺寸數值,即賦值為specSize.


  • 綜上,一個View(or ImageView or TextView ext)測量自身的流程是:

  • 呼叫measure(),並且傳遞相關的測量引數MeasureSpec
  • measure()方法內部繼續呼叫onMeasure()方法,繼續把MeasureSpec傳遞下去
  • onMeasure()方法中,根據自身View型別的不同(ImageView,TexView),以及傳遞進來的MeasureSpec,設定測量的尺寸變數mMeasuredWidth, mMeasureHeight.

  • MeasureSpec在measure過程中的傳遞

    上面我們知道了measure()方法如何根據MeasureSpec來對自身進行度量,但是還有一個疑問,MeasureSpec這麼重要,它似乎就是度量的關鍵,那麼它又從哪裡來?
    我們以LinearLayout為例,都知道LinearLayout是繼承自ViewGroup,當我們呼叫ViewGroup.measure(),它會繼續呼叫子View的measure()方法,只要我們其中的MeasureSpec是如何從父View傳遞給子View,那麼MeasureSpec如何建立以及由什麼決定就可以找到了。

    當呼叫LinearLayout的measure()去度量,會再自動呼叫onMeasure()來測量(已經被LinearLayout類重寫),如下:

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

    再進入measureHorizontal()方法

       /**
         * Measures the children when the orientation of this LinearLayout is set
         * to {@link #HORIZONTAL}.
         *
         * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
         * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
         *
         * @see #getOrientation()
         * @see #setOrientation(int)
         * @see #onMeasure(int, int) 
         */
        void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        // 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).
                    measureChildBeforeLayout(child, i, widthMeasureSpec,
                            totalWeight == 0 ? mTotalLength : 0,
                            heightMeasureSpec, 0);
        ...
        }

    發現在measureHorizontal()方法中最後會呼叫measureChildBeforeLayout()方法

        /**
         * <p>Measure the child according to the parent's measure specs. This
         * method should be overriden by subclasses to force the sizing of
         * children. This method is called by {@link #measureVertical(int, int)} and
         * {@link #measureHorizontal(int, int)}.</p>
         *
         * @param child the child to measure
         * @param childIndex the index of the child in this view
         * @param widthMeasureSpec horizontal space requirements as imposed by the parent
         * @param totalWidth extra space that has been used up by the parent horizontally
         * @param heightMeasureSpec vertical space requirements as imposed by the parent
         * @param totalHeight extra space that has been used up by the parent vertically
         */
        void measureChildBeforeLayout(View child, int childIndex,
                int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
                int totalHeight) {
            measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                    heightMeasureSpec, totalHeight);
        }

    繼續追蹤measureChildBeforeLayout(),發現它又呼叫了ViewGroup.measureChildWithMargins()方法

        /**
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding
         * and margins. The child must have MarginLayoutParams The heavy lifting is
         * done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param widthUsed Extra space that has been used up by the parent
         *        horizontally (possibly by other children of the parent)
         * @param parentHeightMeasureSpec The height requirements for this view
         * @param heightUsed Extra space that has been used up by the parent
         *        vertically (possibly by other children of the parent)
         */
        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);
        }

    在measureChildWithMargins()方法中,很明顯最後一行呼叫了子View的measure()方法測量子View的寬高,這裡傳遞給子View的MeasureSpec數值又經過了一番計算,這裡很明顯,傳遞給子View的MeasureSpec數值是由於子View的LayoutParams和父View傳下來的MeasureSpec共同決定的。

    通過ViewGroup.getChildMeasureSpec()方法,根據父佈局傳入的MeasureSpec和view自身的LayoutParams計算出應該傳遞給view的MeasureSpec的數值

        /**
         * 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;
            }
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    我們來看ViewGroup.getChildMeasureSpec()計算MeasureSpec的邏輯,由父View的MeasureSpec數值和自身的LayoutParams數值共同決定,具體的邏輯處理可見下表:
    (圖片來自網路)
    這裡寫圖片描述

    測試例子

    測量的開始是從呼叫measure()方法開始,而當我構造一個View的例項時,它的measure()並不會被馬上呼叫,那麼當我們想要預先知道一個View的寬高的時候,我們可以不需要等待View的測量繪製流程,直接手動呼叫measure(),測量View的尺寸資料。

        /**
         * 測量view的尺寸,實際上view的最終尺寸會由於父佈局傳遞來的MeasureSpec和view本身的LayoutParams共同決定
         * 這裡預先測量,由自己給出的MeasureSpec計算尺寸
         * @param view
         */
        public static void measure(View view) {
            int sizeWidth, sizeHeight, modeWidth, modeHeight;
            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            if (layoutParams == null) {
                layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            }
            if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                sizeWidth = 0;
                modeWidth = View.MeasureSpec.UNSPECIFIED;
            } else {
                sizeWidth = layoutParams.width;
                modeWidth = View.MeasureSpec.EXACTLY;
            }
            if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                sizeHeight = 0;
                modeHeight = View.MeasureSpec.UNSPECIFIED;
            } else {
                sizeHeight = layoutParams.height;
                modeHeight = View.MeasureSpec.EXACTLY;
            }
            view.measure(View.MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth),
                    View.MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight)
            );
        }

    我們來做一個測驗:

            TextView textView = (TextView) findViewById(R.id.text);
    
            Log.d(TAG, "Before Measure the view");
            Log.d(TAG, "width = " + textView.getMeasuredWidth());
            Log.d(TAG, "height = " + textView.getMeasuredHeight());
    
    
            MeasureUtils.measure(textView);
            Log.d(TAG, "After Measure the view");
            Log.d(TAG, "width = " + textView.getMeasuredWidth());
            Log.d(TAG, "height = " + textView.getMeasuredHeight());

    結果輸出:

    08-13 02:50:58.714 2548-2548/? D/Measure: Before Measure the view
    08-13 02:50:58.714 2548-2548/? D/Measure: width = 0
    08-13 02:50:58.714 2548-2548/? D/Measure: height = 0
    08-13 02:50:58.715 2548-2548/? D/Measure: After Measure the view
    08-13 02:50:58.715 2548-2548/? D/Measure: width = 24
    08-13 02:50:58.715 2548-2548/? D/Measure: height = 19

    從這個測試例子,我們也可以驗證之前的說法,View的測量時從外部呼叫View的共有方法measure()開始的,但是呼叫的時機並不是我們一初始化View它就開始,所以一開始初始化之後,mMeasuredWidth和mMeasuredHeight的值都為預設的 0 , 但是當我們主動呼叫measure()方法之後,View就完成了對自身的尺寸的測量。

    onSizeChange()方法獲取寬高

    先來看看View原始碼的onSizeChanged()

        /**
         * This is called during layout when the size of this view has changed. If
         * you were just added to the view hierarchy, you're called with the old
         * values of 0.
         *
         * @param w Current width of this view.
         * @param h Current height of this view.
         * @param oldw Old width of this view.
         * @param oldh Old height of this view.
         */
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        }

    通過程式碼檢視,我們尋找View類中哪裡呼叫了這個方法:

        private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
            onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
            if (mOverlay != null) {
                mOverlay.getOverlayView().setRight(newWidth);
                mOverlay.getOverlayView().setBottom(newHeight);
            }
            rebuildOutline();
        }

    繼續檢視,哪裡呼叫了sizeChange():

     /**
         * Sets the top position of this view relative to its parent. This method is meant to be called
         * by the layout system and should not generally be called otherwise, because the property
         * may be changed at any time by the layout.
         *
         * @param top The top of this view, in pixels.
         */
        public final void setTop(int top) {
        ...
        sizeChange(width, mBottom - mTop, width, oldHeight);
        ...
        }

    setTop()方法用來設定View的top position的值,它被layout system呼叫,當View的尺寸屬性發生變化,會呼叫,同理我們也可以在setBottom(),setLeft(),setRight()方法中看到sizeChange()被呼叫。

    因此,當onSizeChanged()呼叫的時候,我們可以從中獲取到當前View的高度和寬度。

    其他方法

    其實通過在onSizeChanged()方法中獲取width和height,就是等待View的度量和繪製工作完成,相比第一種方式,這種方式較為被動,但是獲取的數值也較為準確。

    同理,我們也可以在onDraw()方法中獲取width和height,這時候View的度量工作已經完成,我們能夠獲取到View已經測量好的寬度和高度。

    另外在網上看到,我們還可以使用以下方式來獲取寬高:

            view.getViewTreeObserver().addOnGlobalLayoutListener(
                    new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    view.getMeasuredWidth();
                    view.getMeasuredHeight();
                }
            });