1. 程式人生 > >Android自定義View-Measure原理篇

Android自定義View-Measure原理篇

在自定義View中有時需要測量View的尺寸,因此,瞭解View的Measure過成有助於我們開發自定義View。

一、目的:測量View的寬與高

在有些情況下,需要多次測量(measure)才能夠最終確定View的寬高(比如父檢視MeasureSpec使用UNSPECIFIED模式等),在這種情況下,通過onMeasure方法獲得的寬高很可能是不準確的,因此,《Android開發藝術探索》建議在onLayout方法中去獲取View的最終寬高。

二、基礎:在開始瞭解measure過程之前,我們需要對兩個傳遞尺寸的類做個瞭解

1.ViewGroup.LayoutParams類:佈局引數類

作用:設定檢視的寬度和高度等佈局引數

引數 解釋
fill_parent 與父檢視等高(2.3之前使用)
match_parent 同fill_parent,2.3及之後版本使用
wrap_parent 自適應大小
具體值

dp/px

2.MeasureSpec類:測量規格類,測量View大小的依據

(1)作用:決定一個檢視View的尺寸

(2)型別:①寬測量規格widthMeasureSpec、②高測量規格heightMeasureSpec

(3)組成:測量規格(MeasureSpec,32位,int型別) = 測量模式(mode,高2位) + 測量大小(size,低30位)

測量模式mode型別

        相關原始碼如下:

public class MeasureSpec {

        // 進位大小 = 2的30次方
        // int的大小為32位,所以進位30位 = 使用int的32和31位做標誌位
        private static final int MODE_SHIFT = 30;  
          
        // 運算遮罩:0x3為16進位制,10進製為3,二進位制為11
        // 3向左進位30 = 11 00000000000(11後跟30個0)  
        // 作用:用1標註需要的值,0標註不要的值。因1與任何數做與運算都得任何數、0與任何數做與運算都得0
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
  
        // UNSPECIFIED的模式設定:0向左進位30 = 00後跟30個0,即00 00000000000
        // 通過高2位
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        
        // EXACTLY的模式設定:1向左進位30 = 01後跟30個0 ,即01 00000000000
        public static final int EXACTLY = 1 << MODE_SHIFT;  

        // AT_MOST的模式設定:2向左進位30 = 10後跟30個0,即10 00000000000
        public static final int AT_MOST = 2 << MODE_SHIFT;  
  
        /**
          * makeMeasureSpec()方法
          * 作用:根據提供的size和mode得到一個詳細的測量結果嗎,即measureSpec
          **/ 
            public static int makeMeasureSpec(int size, int mode) {  
            
                return size + mode;  
            // measureSpec = size + mode;此為二進位制的加法 而不是十進位制
            // 設計目的:使用一個32位的二進位制數,其中:32和31位代表測量模式(mode)、後30位代表測量大小(size)
            // 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100  

            }  
      
        /**
          * getMode()方法
          * 作用:通過measureSpec獲得測量模式(mode)
          **/    

            public static int getMode(int measureSpec) {  
             
                return (measureSpec & MODE_MASK);  
                // 即:測量模式(mode) = measureSpec & MODE_MASK;  
                // MODE_MASK = 運算遮罩 = 11 00000000000(11後跟30個0)
                //原理:保留measureSpec的高2位(即測量模式)、使用0替換後30位
                // 例如10 00..00100 & 11 00..00(11後跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值

            }  
        /**
          * getSize方法
          * 作用:通過measureSpec獲得測量大小size
          **/       
            public static int getSize(int measureSpec) {  
             
                return (measureSpec & ~MODE_MASK);  
                // size = measureSpec & ~MODE_MASK;  
               // 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00後跟30個1),將32,31替換成0也就是去掉mode,保留後30位的size  
            } 

    }

(5)計算:子View的具體大小由父View的MeasureSpec值和子View的LayoutParams屬性共同決定,即:

MeasureSpec值計算過程

具體的計算封裝在getChildMeasureSpec裡,原始碼如下:

/**
  * 原始碼分析:getChildMeasureSpec()
  * 作用:根據父檢視的MeasureSpec & 佈局引數LayoutParams,計算單個子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
  **/

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //引數說明
         * @param spec 父view的詳細測量值(MeasureSpec) 
         * @param padding view當前尺寸的的內邊距和外邊距(padding,margin) 
         * @param childDimension 子檢視的佈局引數(寬/高)

            //父view的測量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     
          
            //通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)   
            int size = Math.max(0, specSize - padding);  
          
            //子view想要的實際大小和模式(需要計算)  
            int resultSize = 0;  
            int resultMode = 0;  
          
            //通過父view的MeasureSpec和子view的LayoutParams確定子view的大小  


            // 當父view的模式為EXACITY時,父view強加給子view確切的值
           //一般是父view設定為match_parent或者固定值的ViewGroup 
            switch (specMode) {  
            case MeasureSpec.EXACTLY:  
                // 當子view的LayoutParams>0,即有確切的值  
                if (childDimension >= 0) {  
                    //子view大小為子自身所賦的值,模式大小為EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 當子view的LayoutParams為MATCH_PARENT時(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小為父view大小,模式為EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 當子view的LayoutParams為WRAP_CONTENT時(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view決定自己的大小,但最大不能超過父view,模式為AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 當父view的模式為AT_MOST時,父view強加給子view一個最大的值。(一般是父view設定為wrap_content)  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 當父view的模式為UNSPECIFIED時,父容器不對view有任何限制,要多大給多大
            // 多見於ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小為子自身所賦的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因為父view為UNSPECIFIED,所以MATCH_PARENT的話子類大小為0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因為父view為UNSPECIFIED,所以WRAP_CONTENT的話子類大小為0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        }

對此,我們可以總結出如下規律:

這裡需要注意的是,頂級View,即DecorView的測量規格=自身佈局引數+視窗尺寸

三、measure過程

measure會根據View的型別分成兩種情況

measure情況分析
View型別 measure過程
單一View 只測量自身一個View
ViewGroup 對ViewGroup檢視中所有的子View都進行測量

(1)我們首先來看下單一View的measure過程

場景分析:在現有View無法滿足需求,需要自己實現時使用自定義單一View

具體使用:繼承View、SurfaceView或其他View

具體流程:measure()→onMeasure()→setMeasureDimension()→getDefaultSize()

先來看一下這幾個方法:

/**
  * 原始碼分析:measure()
  * 定義:Measure過程的入口;屬於View.java類 & final型別,即子類不能重寫此方法
  * 作用:基本測量邏輯的判斷
  **/ 

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

        // 引數說明:View的寬 / 高測量規格

        ...

        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);

        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 計算檢視大小 ->>分析1

        } else {
            ...
      
    }

/**
  * 分析1:onMeasure()
  * 作用:a. 根據View寬/高的測量規格計算View的寬/高值:getDefaultSize()
  *      b. 儲存測量後的View寬 / 高:setMeasuredDimension()
  **/ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 引數說明:View的寬 / 高測量規格

    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :獲得View寬/高的測量值 ->>分析2
    // 傳入的引數通過getDefaultSize()獲得 ->>分析3
}

/**
  * 分析2:setMeasuredDimension()
  * 作用:儲存測量後的View寬 / 高
  * 注:該方法即為我們重寫onMeasure()所要實現的最終目的
  **/
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  

    //引數說明:測量後子View的寬 / 高值

        // 將測量後子View的寬 / 高值進行傳遞
            mMeasuredWidth = measuredWidth;  
            mMeasuredHeight = measuredHeight;  
          
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
        } 
    // 由於setMeasuredDimension()的引數是從getDefaultSize()獲得的
    // 下面我們繼續看getDefaultSize()的介紹

/**
  * 分析3:getDefaultSize()
  * 作用:根據View寬/高的測量規格計算View的寬/高值
  **/
  public static int getDefaultSize(int size, int measureSpec) {  

        // 引數說明:
        // size:提供的預設大小
        // measureSpec:寬/高的測量規格(含模式 & 測量大小)

            // 設定預設大小
            int result = size; 
            
            // 獲取寬/高測量規格的模式 & 測量大小
            int specMode = MeasureSpec.getMode(measureSpec);  
            int specSize = MeasureSpec.getSize(measureSpec);  
          
            switch (specMode) {  
                // 模式為UNSPECIFIED時,使用提供的預設大小 = 引數Size
                case MeasureSpec.UNSPECIFIED:  
                    result = size;  
                    break;  

                // 模式為AT_MOST,EXACTLY時,使用View測量後的寬/高值 = measureSpec中的Size
                case MeasureSpec.AT_MOST:  
                case MeasureSpec.EXACTLY:  
                    result = specSize;  
                    break;  
            }  

         // 返回View的寬/高值
            return result;  
        }

在onMeasure方法中,我們可以知道函式getSuggestedMinimumWidth()是獲取預設大小,那麼,我們可以看一下它具體的實現:

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

從這裡我們可以知道,當View沒有設定背景時,View寬度=mMinWidth(如果android:minWidth沒有指定,則為0,否則為該屬性所設定的值),如果View設定了背景,則View寬度為mMinWidth和mBackground.getMinimumWidth()中的最大值。

對於以上過程,我們可以使用下圖理一下邏輯:

至此,單一View measure過程完成,總結一下:

單一View measure過程

(2)ViewGroup的measure過程

場景分析:利用現有元件來組成新的元件

具體使用:繼承自ViewGroup或各種Layout,可以含有子View

具體流程:measure()→onMeasure()(需要複寫)→measureChildren()→measureChild()→getChildMeasureSpec()→遍歷子View測量併合並→setMeasureDimension()

具體原始碼分析如下:

/**
  * 原始碼分析:measure()
  * 作用:基本測量邏輯的判斷;呼叫onMeasure()
  * 注:與單一View measure過程中講的measure()一致
  **/ 
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
            mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {

        // 呼叫onMeasure()計算檢視大小
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
        ...
}

/**
  * 根據自身的測量邏輯複寫onMeasure(),分為3步
  * 1. 遍歷所有子View & 測量:measureChildren()
  * 2. 合併所有子View的尺寸大小,最終得到ViewGroup父檢視的測量值(自身實現)
  * 3. 儲存測量後View寬/高的值:呼叫setMeasuredDimension()  
  **/ 

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

        // 定義存放測量後的View寬/高的變數
        int widthMeasure ;
        int heightMeasure ;

        // 1. 遍歷所有子View & 測量(measureChildren())
        // ->> 分析1
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        // 2. 合併所有子View的尺寸大小,最終得到ViewGroup父檢視的測量值
         void measureCarson{
             ... // 自身實現
         }

        // 3. 儲存測量後View寬/高的值:呼叫setMeasuredDimension()  
        // 類似單一View的過程,此處不作過多描述
        setMeasuredDimension(widthMeasure,  heightMeasure);  
  }
  // 從上可看出:
  // 複寫onMeasure()有三步,其中2步直接呼叫系統方法
  // 需自身實現的功能實際僅為步驟2:合併所有子View的尺寸大小

/**
  * 分析1:measureChildren()
  * 作用:遍歷子View & 呼叫measureChild()進行下一步測量
  **/ 

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        // 引數說明:父檢視的測量規格(MeasureSpec)

                final int size = mChildrenCount;
                final View[] children = mChildren;

                // 遍歷所有子view
                for (int i = 0; i < size; ++i) {
                    final View child = children[i];
                     // 呼叫measureChild()進行下一步的測量 ->>分析1
                    if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                        measureChild(child, widthMeasureSpec, heightMeasureSpec);
                    }
                }
            }

/**
  * 分析2:measureChild()
  * 作用:a. 計算單個子View的MeasureSpec
  *      b. 測量每個子View最後的寬 / 高:呼叫子View的measure()
  **/ 
  protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        // 1. 獲取子檢視的佈局引數
        final LayoutParams lp = child.getLayoutParams();

        // 2. 根據父檢視的MeasureSpec & 佈局引數LayoutParams,計算單個子View的MeasureSpec
        // getChildMeasureSpec() 請看上面第2節儲備知識處
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 獲取 ChildView 的 widthMeasureSpec
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 獲取 ChildView 的 heightMeasureSpec
                mPaddingTop + mPaddingBottom, lp.height);

        // 3. 將計算好的子View的MeasureSpec值傳入measure(),進行最後的測量
        // 下面的流程即類似單一View的過程,此處不作過多描述
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    // 回到呼叫原處

為了更好理解,我們可以看一下LinearLayout複寫的onMeasure程式碼分析:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

      // 根據不同的佈局屬性進行不同的計算
      // 此處只選垂直方向的測量過程,即measureVertical()->>分析1
      if (mOrientation == VERTICAL) {
          measureVertical(widthMeasureSpec, heightMeasureSpec);
      } else {
          measureHorizontal(widthMeasureSpec, heightMeasureSpec);
      }

      
}

  /**
    * 分析1:measureVertical()
    * 作用:測量LinearLayout垂直方向的測量尺寸
    **/ 
  void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
      
      /**
       *  其餘測量邏輯
       **/
          // 獲取垂直方向上的子View個數
          final int count = getVirtualChildCount();

          // 遍歷子View獲取其高度,並記錄下子View中最高的高度數值
          for (int i = 0; i < count; ++i) {
              final View child = getVirtualChildAt(i);

              // 子View不可見,直接跳過該View的measure過程,getChildrenSkipCount()返回值恆為0
              // 注:若view的可見屬性設定為VIEW.INVISIBLE,還是會計算該view大小
              if (child.getVisibility() == View.GONE) {
                 i += getChildrenSkipCount(child, i);
                 continue;
              }

              // 記錄子View是否有weight屬性設定,用於後面判斷是否需要二次measure
              totalWeight += lp.weight;

              if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                  // 如果LinearLayout的specMode為EXACTLY且子View設定了weight屬性,在這裡會跳過子View的measure過程
                  // 同時標記skippedMeasure屬性為true,後面會根據該屬性決定是否進行第二次measure
                // 若LinearLayout的子View設定了weight,會進行兩次measure計算,比較耗時
                  // 這就是為什麼LinearLayout的子View需要使用weight屬性時候,最好替換成RelativeLayout佈局
                
                  final int totalLength = mTotalLength;
                  mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                  skippedMeasure = true;
              } else {
                  int oldHeight = Integer.MIN_VALUE;
     /**
       *  步驟1:遍歷所有子View & 測量:measureChildren()
       *  注:該方法內部,最終會呼叫measureChildren(),從而 遍歷所有子View & 測量
       **/
            measureChildBeforeLayout(

                   child, i, widthMeasureSpec, 0, heightMeasureSpec,
                   totalWeight == 0 ? mTotalLength : 0);
                   ...
            }

      /**
       *  步驟2:合併所有子View的尺寸大小,最終得到ViewGroup父檢視的測量值(自身實現)
       **/        
              final int childHeight = child.getMeasuredHeight();

              // 1. mTotalLength用於儲存LinearLayout在豎直方向的高度
              final int totalLength = mTotalLength;

              // 2. 每測量一個子View的高度, mTotalLength就會增加
              mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                     lp.bottomMargin + getNextLocationOffset(child));
      
              // 3. 記錄LinearLayout佔用的總高度
              // 即除了子View的高度,還有本身的padding屬性值
              mTotalLength += mPaddingTop + mPaddingBottom;
              int heightSize = mTotalLength;

      /**
       *  步驟3:儲存測量後View寬/高的值:呼叫setMeasuredDimension()  
       **/ 
       setMeasureDimension(resolveSizeAndState(maxWidth,width))

    ...

  }

到這裡,ViewGroup的measure講完了,我們來總結一下:

ViewGroup measure分析過程