Android 淺談View的測量measure

golj9515 8年前發布 | 17K 次閱讀 安卓開發 Android開發 移動開發

一般一個View的呈現基本需要三大流程measure、layout、draw,measure作為View的三大工作流程之一,也是三大流程中第一個流程,主要用于確定View的測量寬/高,該流程的執行情況將直接影響后續的兩個流程,可謂是重中之重,不可不察也。其余的兩個流程layout用于確定View的最終寬高和四個頂點的位置,Draw則將View繪制到屏幕上。

講到View的measure測量,一般會涉及到兩個方法和一個類,兩個方法分別是measure和onMeasure,一個類是MeasureSpec。在自定義View中MeasureSpec在measure和onMeasure兩個方法中都有使用,所以為了更好地理解View的測量過程,MeasureSpec是我們首先需要理解的東西。

MeasureSpec

MeasureSpec是View的一個靜態內部類,MeasureSpec類封裝了父View傳遞給子View的布局(layout)要求,每個MeasureSpec實例代表寬度或者高度(只能是其一)要求。MeasureSpec字面意思是測量規格或者測量屬性,在measure方法中有兩個參數widthMeasureSpec和heightMeasureSpec,如果使用widthMeasureSpec,我們就可以通過MeasureSpec計算出寬的模式Mode和寬度的實際值,當然了也可以通過模式Mode和寬度獲得一個MeasureSpec,下面是MeasureSpec的部分核心邏輯。

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;
 
    // 0向左進位30,就是00 00000000000(00后跟30個0)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    // 1向左進位30,就是01 00000000000(01后跟30個0)
    public static final int EXACTLY    = 1 << MODE_SHIFT;
    // 2向左進位30,就是10 00000000000(10后跟30個0)
    public static final int AT_MOST    = 2 << MODE_SHIFT;
 
    /**
     * 根據提供的size和mode得到一個詳細的測量結果
     */
    // measureSpec = size + mode; (注意:二進制的加法,不是10進制的加法!)
    // 這里設計的目的就是使用一個32位的二進制數,32和31位代表了mode的值,后30位代表size的值
    // 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100
    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }
 
    /**
     * 通過詳細測量結果獲得mode
     */
    // mode = measureSpec & MODE_MASK;
    // MODE_MASK = 11 00000000000(11后跟30個0),原理是用MODE_MASK后30位的0替換掉measureSpec后30位中的1,再保留32和31位的mode值。
    // 例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
 
    /**
     * 通過詳細測量結果獲得size
     */
    // size = measureSpec & ~MODE_MASK;
    // 原理同上,不過這次是將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),將32,31替換成0也就是去掉mode,保留后30位的size
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

}

MeasureSpec實際上是對int類型的整數進行位運算的一個封裝,其中前2位是Mode,后面30位是實際寬或高,Mode就三種情況:

  • UNSPECIFIED(未指定) 父元素不會對子元素施加任何束縛,子元素可以得到任意想要的大小;
  • EXACTLY(完全) 父元素決定子元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身大小;
  • AT_MOST(至多) 子元素至多達到指定大小的值。

三種模式中最常用的是EXACTLY和AT_MOST兩種模式,這兩種模式分別對應layout布局文件中的match_parent和wrap_content,而布局文件會轉化為中layout相關屬性會轉換為LayoutParams,接下來我們看一下LayoutParams是如何與MeasureSpec進行邏輯交互的。

LayoutParams與MeasureSpec關系

系統內部通過MeasureSpec對View進行測量,但是我們可以通過給View設置LayoutParams來影響MeasureSpec,有關LayoutParams的更多內容可以查看 Android淺談LayoutParams 。在View測量的時候,系統會將LayoutParams在父ViewGroup的作用下轉化為MeasureSpec,這里需要注意一點子View的MeasureSpec不是唯一有LayoutParams決定而是與父ViewGroup的MeasureSpec一起決定。在ViewGroup中無論是measureChild還是measureChildWithMargins方法中都有一個getChildMeasureSpec方法,代碼如下:

protected void measureChild(Viewchild, int parentWidthMeasureSpec,
 int parentHeightMeasureSpec) {
 final LayoutParamslp = child.getLayoutParams();
 
 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 mPaddingLeft + mPaddingRight, lp.width);
 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
 mPaddingTop + mPaddingBottom, lp.height);
 
 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
 
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的MeasureSpec和子View的LayoutParams就可以很快的確定出子View的MeasureSpec,有了MeasureSpec就可以很快確定出子View測量后的大小了。講到這里會發現還有一種模式沒有說明呢,UNSPECIFIED這種模式在下文結合代碼再繼續講解,該模式主要用于系統內部多次measure的情形。

measure方法

如果只是一個View直接通過measure就可以完成測量過程,但是如果是一個ViewGroup,除了完成自己的測量外,還需要遍歷測量自己的所有孩子,各個子元素都需要遞歸調用該過程直至所有孩子都測量完畢。

在直接繼承自ViewGroup中自定義View中,一般我們都需要重寫一個onMeasure方法,但是該方法不是必須的,通過代碼可以很容易發現,因為需要我們強制重寫的方法中并沒有onMeasure方法,這是因為如果我們的自定義ViewGroup中子View的大小是ViewGroup直接分配的,并沒有考慮子View自身大小因素,比如我們需要自定義一個相冊View,每一行顯示三個圖片,這時候只要三個圖片平均分配占滿一行就可以了,不用考慮子View大小,由父ViewGroup直接賦值就可以了。但是在自定義ViewGroup時,如果想要測量子View,都是直接調用的measure方法,但是當前類中需要重寫的確是onMeasure方法,這是為什么呢?先看一下View中measure方法的定義:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
 if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
 
 //...
 
            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
            } 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);
            }
        }
 //...
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
}

從measure方法定義格式就可以知道,我們想重寫該方法都不行因為是最終方法。再看修飾符是 public ,這也就意味著我們可以在任意的地方View都可以直接調用measure方法,這也是為什么有時候在一些demo代碼會看到measure(0,0)這種奇怪的調用方式了,因為View只有被測量過可以知道其大小,還沒被測量之前如果想知道View大小怎么辦呢,那么手動測量一下就可以了,那么如果我多次調用measure方法會不會測量多次呢,這個不一定,有上面代碼可以知道,當測量完成以后View的寬高值會存入一個mMeasureCache的變量中,當我們再次傳入的MeasureSpec相同,,此時變回直接從mMeasureCache中將上一次存入的值直接取出來賦值到View中。

measure(0,0)中0代表的是什么?從measure方法的傳參類型可以知曉0其實就是一個值為0的MeasureSpec,該MeasureSpec對應的模式就是UNSPECIFIED,上面說了該模式父View不會對子View添加任何限制,子View可以任意大小,這個任意大小就是子View不受父View空間約束的實際大小。下面我們通過onMeasure方法中的邏輯梳理一下measure(0,0)。

onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
 
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
 boolean optical = isLayoutModeOptical(this);
 if (optical != isLayoutModeOptical(mParent)) {
 Insetsinsets = getOpticalInsets();
 int opticalWidth  = insets.left + insets.right;
 int opticalHeight = insets.top  + insets.bottom;
 
 measuredWidth  += optical ? opticalWidth  : -opticalWidth;
 measuredHeight += optical ? opticalHeight : -opticalHeight;
 }
 setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
 
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
 mMeasuredWidth = measuredWidth;
 mMeasuredHeight = measuredHeight;
 
 mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

setMeasuredDimension就是設置View的寬高值,核心還是看getDefaultSize方法。

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代碼可以知道,如果傳入的measureSpec的模式是UNSPECIFIED,那么View的大小就是傳入值size的大小,計算size代碼如下:

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

從代碼可以看出如果View沒有設置背景,那么View的大小就是mMinWidth,mMinWidth是在layout布局文件中設置的android:minWidth指定的值,如果這個值沒有指定,則最總返回0。

通過getDefaultSize的實現可以知道,View的寬高由specSize決定,我們可以得出結論:直接繼承View的自定義控件需要重寫onMeasure方法并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent,為什么是這樣呢?當我們在布局中使用wrap_content時,那么它的specMode相當于AT_MOST,而在這種模式下它的寬高等于specSize,而這個specSize是通過父View傳入的MeasureSpec獲取到的,事實上就是父View的可以使用的大小,也是父View剩余空間的大小。很顯然這種情況下View的寬高等于父View剩余空間的大小,跟在布局中使用match_parent效果完全一致。這個問題也容易解決,通過效仿getSuggestedMinimumWidth方法,給View設置一個內部的默認的寬高,當設置為wrap_content直接使用設置的默認寬高即可。對于非wrap_content,我們仍然使用系統內部的測量值即可。處理wrap_content時示例代碼如下:

public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
 if(widthMode== MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
 setMeasuredDimension(mWidth,mHeight);
 }else if(widthMode== MeasureSpec.AT_MOST){
 setMeasuredDimension(mWidth,heightSize);
 }else if(heightMode==MeasureSpec.AT_MOST){
 setMeasuredDimension(widthSize,mHeight);
 }
}

 

來自:http://www.sunnyang.com/585.html

 

 本文由用戶 golj9515 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!