Carson帶你學(xué)Android:自定義View 測(cè)量過(guò)程(Measure)


前言

  • 自定義ViewAndroid開發(fā)者必須了解的基礎(chǔ)
  • 網(wǎng)上有大量關(guān)于自定義View原理的文章,但存在一些問(wèn)題:內(nèi)容不全、思路不清晰、無(wú)源碼分析、簡(jiǎn)單問(wèn)題復(fù)雜化 等
  • 今天,我將全面總結(jié)自定義View原理中的measure過(guò)程,我能保證這是市面上的最全面、最清晰、最易懂的

Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過(guò)程
Carson帶你學(xué)Android:自定義View Layout過(guò)程
Carson帶你學(xué)Android:自定義View Draw過(guò)程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析


目錄

示意圖

1. 作用

測(cè)量View的寬 / 高

  1. 在某些情況下,需要多次測(cè)量(measure)才能確定View最終的寬/高;
  2. 該情況下,measure過(guò)程后得到的寬 / 高可能不準(zhǔn)確;
  3. 此處建議:在layout過(guò)程中onLayout()去獲取最終的寬 / 高

2. 儲(chǔ)備知識(shí)

了解measure過(guò)程前,需要3個(gè)儲(chǔ)備知識(shí):

  1. 自定義View基礎(chǔ)知識(shí)
  2. ViewGroup.LayoutParams類()
  3. MeasureSpecs

2.1 最基本的知識(shí)儲(chǔ)備

具體請(qǐng)看文章:自定義View基礎(chǔ) - 最易懂的自定義View原理系列

2.2 ViewGroup.LayoutParams

  • 簡(jiǎn)介
    布局參數(shù)類
  1. ViewGroup 的子類(RelativeLayout、LinearLayout)有其對(duì)應(yīng)的 ViewGroup.LayoutParams 子類
  2. 如:RelativeLayoutViewGroup.LayoutParams子類
    = RelativeLayoutParams
  • 作用
    指定視圖View 的高度(height) 和 寬度(width)等布局參數(shù)。
  • 具體使用
    通過(guò)以下參數(shù)指定
參數(shù) 解釋
具體值 dp / px
fill_parent 強(qiáng)制性使子視圖的大小擴(kuò)展至與父視圖大小相等(不含 padding )
match_parent 與fill_parent相同,用于Android 2.3 & 之后版本
wrap_content 自適應(yīng)大小,強(qiáng)制性地使視圖擴(kuò)展以便顯示其全部?jī)?nèi)容(含 padding )
android:layout_height="wrap_content"   //自適應(yīng)大小  
android:layout_height="match_parent"   //與父視圖等高  
android:layout_height="fill_parent"    //與父視圖等高  
android:layout_height="100dip"         //精確設(shè)置高度值為 100dip  
  • 構(gòu)造函數(shù)
    構(gòu)造函數(shù) = View的入口,可用于初始化 & 獲取自定義屬性
// View的構(gòu)造函數(shù)有四種重載
    public DIY_View(Context context){
        super(context);
    }

    public DIY_View(Context context,AttributeSet attrs){
        super(context, attrs);
    }

    public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ){
        super(context, attrs,defStyleAttr);

// 第三個(gè)參數(shù):默認(rèn)Style
// 默認(rèn)Style:指在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style
// 且只有在明確調(diào)用的時(shí)候才會(huì)生效,
    }
    
    public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ,int defStyleRes){
        super(context, attrs,defStyleAttr,defStyleRes);
    }

// 最常用的是1和2
}

2.3 MeasureSpec

示意圖

具體請(qǐng)看文章:Android自定義View基礎(chǔ):MeasureSpec類到底是什么?


3. measure過(guò)程詳解

measure過(guò)程 根據(jù)View的類型分為2種情況:

示意圖

接下來(lái),我將詳細(xì)分析這兩種measure過(guò)程

3.1 單一View的measure過(guò)程

應(yīng)用場(chǎng)景

在無(wú)現(xiàn)成的控件View滿足需求、需自定義單一View時(shí)。

  1. 如:制作一個(gè)支持加載網(wǎng)絡(luò)圖片的ImageView控件
  2. 注:自定義View在多數(shù)情況下都有替代方案:圖片 / 組合動(dòng)畫,但二者可能會(huì)導(dǎo)致內(nèi)存耗費(fèi)過(guò)大,從而引起內(nèi)存溢出等問(wèn)題。

具體流程

單一View的measure過(guò)程

源碼分析

/**
  * 源碼分析:measure()
  * 定義:Measure過(guò)程的入口;屬于View.java類 & final類型,即子類不能重寫此方法
  * 作用:基本測(cè)量邏輯的判斷
  */ 
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

      // 參數(shù)說(shuō)明:View的寬 / 高測(cè)量規(guī)格
      ...

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

      if (cacheIndex < 0 || sIgnoreMeasureCache) {
          
          onMeasure(widthMeasureSpec, heightMeasureSpec);
          // 計(jì)算視圖大小 ->>分析1
      } else {
          ...
  }

/**
  * 分析1:onMeasure()
  * 作用:a. 根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值:getDefaultSize()
  *      b. 存儲(chǔ)測(cè)量后的View寬 / 高:setMeasuredDimension()
  */ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 參數(shù)說(shuō)明:View的寬 / 高測(cè)量規(guī)格
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :獲得View寬/高的測(cè)量值 ->>分析2
    // 傳入的參數(shù)通過(guò)getDefaultSize()獲得 ->>分析3
}

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

    //參數(shù)說(shuō)明:測(cè)量后子View的寬 / 高值

    // 將測(cè)量后子View的寬 / 高值進(jìn)行傳遞
        mMeasuredWidth = measuredWidth;  
        mMeasuredHeight = measuredHeight;  
      
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
    } 
  // 由于setMeasuredDimension()的參數(shù)是從getDefaultSize()獲得的
  // 下面繼續(xù)看getDefaultSize()的介紹

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

    // 參數(shù)說(shuō)明:
    // size:提供的默認(rèn)大小
    // measureSpec:寬/高的測(cè)量規(guī)格(含模式 & 測(cè)量大?。?
    // 設(shè)置默認(rèn)大小
    int result = size; 
        
    // 獲取寬/高測(cè)量規(guī)格的模式 & 測(cè)量大小
    int specMode = MeasureSpec.getMode(measureSpec);  
    int specSize = MeasureSpec.getSize(measureSpec);  
  
    switch (specMode) {  
        // 模式為UNSPECIFIED時(shí),使用提供的默認(rèn)大小 = 參數(shù)Size
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  

        // 模式為AT_MOST,EXACTLY時(shí),使用View測(cè)量后的寬/高值 = measureSpec中的Size
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  

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

上面提到,當(dāng)測(cè)試規(guī)格的模式(mode)是UNSPECIFIED時(shí),使用的是提供的默認(rèn)大小(即getDefaultSize()的第一個(gè)參數(shù)size)。那么,提供的默認(rèn)大小具體是多少呢?

答:getSuggestedMinimumWidth() / getSuggestedMinimumHeight()。具體請(qǐng)看下面源碼分析。

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

// 邏輯說(shuō)明
// 1. 若View無(wú)設(shè)置背景,那么View的寬度 = mMinWidth
   // 即android:minWidth屬性所指定的值,若無(wú)指定則為0.
// 2. 若View設(shè)置了背景,View的寬度為mMinWidth和mBackground.getMinimumWidth()中的最大值
   // 下面繼續(xù)看mBackground.getMinimumWidth()的源碼分析
 
/**
  * mBackground.getMinimumWidth()源碼分析
  */ 
  public int getMinimumWidth() {

    final int intrinsicWidth = getIntrinsicWidth();
    // 即mBackground.getMinimumWidth()的大小 = 背景圖Drawable的原始寬度
    return intrinsicWidth > 0 ? intrinsicWidth :0 ;
    // 若無(wú)原始寬度,則為0;
}

至此,單一View的寬/高值已經(jīng)測(cè)量完成,即對(duì)于單一View的measure過(guò)程已經(jīng)完成。

源碼總結(jié)

對(duì)于單一View的測(cè)量流程(Measure)各個(gè)方法說(shuō)明如下所示。

測(cè)量寬高的關(guān)鍵在于getDefaultSize(),該方法的測(cè)量邏輯如下圖所示。


3.2 ViewGroup的measure過(guò)程

應(yīng)用場(chǎng)景

利用現(xiàn)有的多個(gè)組件根據(jù)特定的布局方式組成一個(gè)新的組件(即包含多個(gè)子View)。

如:底部導(dǎo)航條中的條目,一般都是上圖標(biāo)(ImageView)、下文字(TextView),那么這兩個(gè)就可以用自定義ViewGroup組合成為一個(gè)Veiw,提供兩個(gè)屬性分別用來(lái)設(shè)置文字和圖片,使用起來(lái)會(huì)更加方便。

示意圖

測(cè)量原理

從ViewGroup至子View、自上而下遍歷進(jìn)行(即樹形遞歸),通過(guò)計(jì)算整個(gè)ViewGroup中各個(gè)View的屬性,從而最終確定整個(gè)ViewGroup的屬性。即:

  1. 遍歷測(cè)量所有子View的尺寸(寬/高);
  2. 合并所有子View的尺寸(寬/高),最終得到ViewGroup父視圖的測(cè)量值。

具體流程

需要特別注意的是:若需進(jìn)行自定義ViewGroup,則需重寫onMeasure(),在下面的章節(jié)會(huì)詳細(xì)講解。

源碼分析

/**
  * 源碼分析:measure()
  * 作用:
  *    1. 基本測(cè)量邏輯的判斷;
  *    2. 調(diào)用onMeasure()
  * 注:與單一View measure過(guò)程中講的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) {

        // 調(diào)用onMeasure()計(jì)算視圖大小 -> 分析1
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
      // ...
}

/**
  * 分析1:onMeasure()
  * 作用:遍歷子View &測(cè)量
  * 注:ViewGroup = 一個(gè)抽象類 = 無(wú)重寫View的onMeasure(),需自身復(fù)寫
  **/

根據(jù)上一小節(jié)可知,單一View的measure過(guò)程對(duì)onMeasure()有統(tǒng)一的實(shí)現(xiàn)(如下代碼),但為什么ViewGroup的measure過(guò)程沒有呢?

/**
  * onMeasure()
  * 作用:a. 根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值:getDefaultSize()
  *      b. 存儲(chǔ)測(cè)量后的View寬 / 高:setMeasuredDimension()
  */ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 參數(shù)說(shuō)明:View的寬 / 高測(cè)量規(guī)格
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :獲得View寬/高的測(cè)量值 ->>分析2
    // 傳入的參數(shù)通過(guò)getDefaultSize()獲得 ->>分析3
}

原因是:onMeasure()方法的作用是測(cè)量View的寬/高值,而不同的ViewGroup(如LinearLayout、RelativeLayout、自定義ViewGroup子類等)具備不同的布局特性,這導(dǎo)致它們的子View測(cè)量方法各有不同,所以onMeasure()的實(shí)現(xiàn)也會(huì)有所不同。

因此,ViewGroup無(wú)法對(duì)onMeasure()作統(tǒng)一實(shí)現(xiàn)。這個(gè)也是單一View的measure過(guò)程與ViewGroup的measure過(guò)程最大的不同。

復(fù)寫onMeasure()

針對(duì)Measure流程,自定義ViewGroup的關(guān)鍵在于:根據(jù)需求復(fù)寫onMeasure(),從而實(shí)現(xiàn)子View的測(cè)量邏輯。復(fù)寫onMeasure()的步驟主要分為三步:

  1. 遍歷所有子View及測(cè)量:measureChildren()
  2. 合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測(cè)量值:需自定義實(shí)現(xiàn)
  3. 存儲(chǔ)測(cè)量后View寬/高的值:setMeasuredDimension()

具體如下所示。

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

      //僅展示關(guān)鍵代碼
      ...

      // 步驟1:遍歷所有子View & 測(cè)量 -> 分析1
      measureChildren(widthMeasureSpec, heightMeasureSpec);

      // 步驟2:合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測(cè)量值
       void measureCarson{
           ... // 需自定義實(shí)現(xiàn)
       }

      // 步驟3:存儲(chǔ)測(cè)量后View寬/高的值
      setMeasuredDimension(widthMeasure,  heightMeasure);  
      // 類似單一View的過(guò)程,此處不作過(guò)多描述
}



/**
  * 分析1:measureChildren()
  * 作用:遍歷子View & 調(diào)用measureChild()進(jìn)行下一步測(cè)量
  */ 
  protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
      // 參數(shù)說(shuō)明:父視圖的測(cè)量規(guī)格(MeasureSpec)

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

      // 遍歷所有子view
      for (int i = 0; i < size; ++i) {

          final View child = children[i];

          // 調(diào)用measureChild()進(jìn)行下一步的測(cè)量 ->分析2
          measureChild(child, widthMeasureSpec, heightMeasureSpec);

      }
  }

/**
  * 分析2:measureChild()
  * 作用:1. 計(jì)算單個(gè)子View的MeasureSpec
  *      2. 測(cè)量每個(gè)子View最后的寬 / 高:調(diào)用子View的measure()
  */ 
  protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {

      // 1. 獲取子視圖的布局參數(shù)
      final LayoutParams lp = child.getLayoutParams();

      // 2. 根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計(jì)算單個(gè)子View的MeasureSpec
      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);

      // 3. 將計(jì)算好的子View的MeasureSpec值傳入measure(),進(jìn)行最后的測(cè)量
      // 下面的流程即類似單一View的過(guò)程,此處不作過(guò)多描述
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 }

至此,ViewGroupmeasure過(guò)程分析完畢


流程總結(jié)

對(duì)于視圖組ViewGroup的測(cè)量流程(Measure)各個(gè)方法說(shuō)明總結(jié)如下所示。

為了讓大家更好地理解ViewGroupmeasure過(guò)程(特別是復(fù)寫onMeasure()),下面,我將用ViewGroup的子類LinearLayout來(lái)分析下ViewGroupmeasure過(guò)程

實(shí)例解析

為了更好理解ViewGroup的measure過(guò)程(特別是復(fù)寫onMeasure()),本小節(jié)將用ViewGroup的子類LinearLayout來(lái)分析ViewGroup的measure過(guò)程。

此處主要分析的是LinearLayout的onMeasure(),具體如下所示。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

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

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

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

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

          // 記錄子View是否有weight屬性設(shè)置,用于后面判斷是否需要二次measure
          totalWeight += lp.weight;

          if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {

            // 如果LinearLayout的specMode為EXACTLY且子View設(shè)置了weight屬性,在這里會(huì)跳過(guò)子View的measure過(guò)程
            // 同時(shí)標(biāo)記skippedMeasure屬性為true,后面會(huì)根據(jù)該屬性決定是否進(jìn)行第二次measure
            // 若LinearLayout的子View設(shè)置了weight,會(huì)進(jìn)行兩次measure計(jì)算,比較耗時(shí)
            // 這就是為什么LinearLayout的子View需要使用weight屬性時(shí)候,最好替換成RelativeLayout布局
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;

          } else {
              
              int oldHeight = Integer.MIN_VALUE;

              // 步驟1:該方法內(nèi)部最終會(huì)調(diào)用measureChildren(),從而 遍歷所有子View & 測(cè)量
              measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec,totalWeight == 0 ? mTotalLength : 0);
              
              ...
            }

        // 步驟2:合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測(cè)量值(需自定義實(shí)現(xiàn))
        final int childHeight = child.getMeasuredHeight();
        // 1. mTotalLength用于存儲(chǔ)LinearLayout在豎直方向的高度
        final int totalLength = mTotalLength;
        // 2. 每測(cè)量一個(gè)子View的高度, mTotalLength就會(huì)增加
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
               lp.bottomMargin + getNextLocationOffset(child));
        // 3. 記錄LinearLayout占用的總高度
        // 即除了子View的高度,還有本身的padding屬性值
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;

        // 步驟3:存儲(chǔ)測(cè)量后View寬/高的值
        setMeasureDimension(resolveSizeAndState(maxWidth,width))
      
        ...
  }

至此,對(duì)于自定義View流程中最重要、最復(fù)雜的測(cè)量流程(measure)分析完畢。


4. 總結(jié)

  • 測(cè)量流程(Measure)根據(jù)視圖(View)的類型分為兩種情況:?jiǎn)我籚iew和視圖組ViewGroup;
  • 二者最大的區(qū)別在于:?jiǎn)我籚iew的measure過(guò)程對(duì)onMeasure()有作統(tǒng)一實(shí)現(xiàn),而ViewGroup的Measuer過(guò)程沒有;
  • 具體測(cè)量流程總結(jié)如下所示

歡迎關(guān)注Carson_Ho的簡(jiǎn)書

不定期分享關(guān)于安卓開發(fā)的干貨,追求短、平、快,但卻不缺深度


請(qǐng)點(diǎn)贊!因?yàn)槟愕墓膭?lì)是我寫作的最大動(dòng)力!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容