1. 程式人生 > >Android自定義View基礎之MeasureSpec詳解

Android自定義View基礎之MeasureSpec詳解

自定義view,首先通過measure layout draw三部曲。measure主要負責測量view的大小和模式,layout主要負責view的顯示位置,draw來將view繪製出來從而顯示在介面上。

首當其衝的就是measure方法,在這個方法裡所做的工作就是測量,那麼首先就先介紹一下一個類MeasureSpec,明確這個後,才能更方便我們自定義view後面的進行。

MeasureSpec含義

首先,先來看一下官方文件對於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.

通過以上資訊的描述,我們可以得到以下三點:

MeasureSpec封裝了父佈局傳遞給子佈局的要求
MeasureSpec代表了需要的寬高值
MeasureSpec是由大小和模式組成的

MeasureSpec一般譯為測量規格,是一個32位的int值。其中高2位代表規格模式,低30位代表測量大小。

  • 獲取測量模式
int mode = MeasureSpec.getMode(measureSpec);
//內部實現為
/**
  * 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); }
  • 獲取測量大小
int size = MeasureSpec.getSize(measureSpec);
//內部實現為
 /**
  * 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
int measureSpec = MeasureSpec.makeMeasureSpec(size, mode);
//內部實現為
/**
  * 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);
     }
 }

MeasureSpec模式

measureSpec一共有三種模式,分別為UNSPECIFIED EXACTLY AT_MOST,下面分別做一下介紹

MeasureSpec.EXACTLY

官方描述為:

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

在此模式下,父容器已經檢測出子view所需要的精確大小,這個時候,view的測量大小就是通過getSize得到的數值。

MeasureSpec.AT_MOST

官方描述為:

/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/

在此模式下,父容器未能檢測出子view的大小,但指定了一個最大大小spec size,子view的大小不能超過此值。

MeasureSpec.UNSPECIFIED

官方描述為:

/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/

在此模式下,父容器不對子view的大小做限制,一般用於系統內部,或者ListView ScrollView等滑動控制元件。

MeasureSpec形成過程

首先,測量子view實在viewgroup中完成的,那麼就從viewgroup的measureChildWithMargins方法開始,看一下measurespec的形成過程。

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 測量目標子view
     * @param parentWidthMeasureSpec The width requirements for this view 父容器寬的measureSpec
     * @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 父容器高的measureSpec
     * @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) {
// 獲取子view的layoutparams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 獲取子view寬的measureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec , mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
// 獲取子view高的measureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec , mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height);
// 將測量出來的值傳遞給子view
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

如上所示,此方法主要有四步,如下:

  • 得到子view的佈局引數
  • 計運算元view寬的measureSpec
  • 計運算元view高的measureSpec
  • 子view進行measure測量

getChildMeasureSpec

在measureChildWithMargins方法中,計運算元view高寬的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 父容器的measureSpec值
 * @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 子view宣告的大小
 * @return a MeasureSpec integer for the child 返回子view測量所需的measureSpec值
*/
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
        // 父容器已經明確字view的大小
        case MeasureSpec.EXACTLY:
        //childDimension指子view在宣告寬高是所指定的具體大小,比如100dp,200dp等,LayoutParams.MATCH_PARENT=-1和LayoutParams.WRAP_CONTENT=-2 
            if (childDimension >= 0) {//子view寬高有具體值,size就是自己宣告的值,mode為精確的
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//子view寬高沒有具體值,但宣告為佔滿父容器的空間,因此它的size就是可用最大空間,mode也為精確的
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子view寬高宣告為包裹內容,那麼其size為父容器在橫或縱空間上可用的最大空間,mode為最大不超過此值
                // 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
        // 父容器未檢測出子view的大小,但是為其size聲明瞭一個最大值
        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) {//同上,填滿父容器,但不能超過父容器的可用空間,mode為at_most模式
                // 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) {//同上,子view為包裹內容,最大值不能超過父容器的可用空間,mode為at_most模式
                // 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
        // 一般應用於系統內部或者listview scrollview等滑動控制元件,不做詳細分析
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {//同上,子view有具體值,so...
                // 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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //生產子view的measureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

如上,巴拉巴拉一通,就確定了子view的size和mode,同時,就確定了傳遞給子view的measureSpec值。

從上面的呼叫過程,我們也可以看出,子view的measureSpec是由父容器的measureSpec和子view本身的layoutParams值共同決定的。
整理上述measureSpec的建立過程,可以得到下圖:

這裡寫圖片描述

measureChild與measureChildWithMargins區別

上面介紹了measureChildWithMargins呼叫getChildMeasureSpec的過程,其實還有一個方法也呼叫了getChildMeasureSpec,那就是measureChild。

  • 共同點
    • 兩者都是測量子view的大小
    • 兩者在呼叫getChildMeasureSpec時都需要計算父容器已佔空間,即mPaddingLeft + mPaddingRight
  • 不同點
    • measureChildWithMargins出了計算父容器已佔空間,還會計運算元view左右兩側margin值,因為這塊區域也是不允許擺放子view的

好了,至此為止,measureSpec含義 模式以及建立規則就基本說完了,下一篇開始,開始介紹onMeasure()等呼叫過程和相關方法。