組件的測量

1、簡介

組件在顯示到界面上要經過三個過程測量、布局、繪制。測量就是計算組件的大小,布局就是擺放組件的位置,而繪制就是把計算好的大小和位置繪制到畫布上。下面就在說說組件測量的那些事。組件的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

2、組件測量的3種類型

widthMeasureSpec和heightMeasureSpec是什么呢?他是一個測量規則,一個32位int值,前兩位表示測量的模式,低30位表示測量的大小。組件也提供對應的方法來獲取他們的值。

final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);

通過size和mode也可以生成對應的測量規則

MeasureSpec.makeMeasureSpec(resultSize, resultMode);

測量模式有3種
EXACTLY
精確模式,當組件的長寬是具體值,如:

android:layout_width="200dp"

或是指定為match_parent(占據父view的大小),系統使用這個模式。

AT_MOST
最大值模式,當控件的長寬屬性為warp_content時,組件的大小隨自身的大小變化而變化,并且控件的尺寸不能超過父view允許最大尺寸。

UNSPECIFIED
不指定組件的大小,組件想多大就多大,通常在繪制自定義view時才會使用。

3、從源碼來看組件測量過程

先來看View源碼里的測量方法:

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

setMeasuredDimension方法接受測量后的長和寬,并賦值給組件,這樣組件就有長寬。那如何計算長寬呢,看下面的代碼:

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

getDefaultSize方法接收兩個參數,一個是大小(系統傳入的是組件的最小寬度或高度getSuggestedMinimumWidth()),一個是測量的規則。
通過測量規則獲得測量的模式和指定的大小,
如果測量模式為MeasureSpec.UNSPECIFIED,則返回傳入的大小為組件大小,及最小寬高;
如果測量模式為MeasureSpec.AT_MOST或MeasureSpec.EXACTLY,則返回父視圖指定的大小。
再來看下ViewGroup中的測量實現,要實現ViewGroup的測量必須實現其子類的測量,但是ViewGroup中并沒有重寫onMeasure方法,需要具體實現類自己重寫,但是他提供一些方法來便于我們測量子視圖。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,            mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

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里會再調用onMeasure方法執行視圖的測量。這里的關鍵實際上就是計算出子視圖的測量規則measureSpec,及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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

這個方法接收3個參數
spec,父視圖的測量規則,可以計算出父視圖的測量模式和測量大小
padding,用于計算扣除padding后的視圖大小
childDimension,視圖布局中定義的寬高大小
通過不同的模式來計算出子視圖的大小和模式從而轉換為測量規則

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:
       ...
        break;
    case MeasureSpec.AT_MOST:
       ...
        break;
    case MeasureSpec.UNSPECIFIED:
        ...
       break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

父視圖為精確模式
1、子視圖有定義具體的大小,resultSize為定義的具體大小,模式為MeasureSpec.EXACTLY
2、子視圖的childDimension為LayoutParams.MATCH_PARENT,resultSize為定義的具體大小,模式為MeasureSpec.EXACTLY
3、子視圖的childDimension為LayoutParams.WRAP_CONTEN,resultSize為定義的具體大小,模式為MeasureSpec.AT_MOST
其他兩種情況也是類似,源碼中已經描述的很清楚了。最后調用MeasureSpec.makeMeasureSpec(resultSize, resultMode);方法就獲得了子視圖的測量規則。
再回顧前面說的View的測量,就能確定具體的View的測量大小。視圖的整個測量過程就是這樣,關鍵還是在要理解清楚MeasureSpec測量規則的使用。

4、測量在實際開發中的應用

 具有最大高度的滾動視圖
 通過計算ScrollView子視圖的高度,超過設置的最大高度,則設置Scrollview的高度為最大高度,如果沒有超過,則按子視圖的高度來顯示scrollview。

具體的代碼如下:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int mMaxHeight = height > maxHeight ? maxHeight : height;
    View child = getChildAt(0);
    int child_height = child.getHeight();
    if (child_height == 0)
        child_height = mMaxHeight;
    else if (child_height > maxHeight)
        child_height = maxHeight;
    setMeasuredDimension(width, child_height);
}

5、注意事項

1、要區分getMeasuredHeight()與getHeight()的區別,getMeasuredHeight()是測量時的大小,組件有可能會測量多次,多次測量的大小有可能不一樣,getHeight()是要繪制的大小
2、視圖只有測量過后,getMeasuredHeight()方法才會有值,不然返回的是0

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容