Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 解讀自定義viewgroup在計算測量上的部分源碼

解讀自定義viewgroup在計算測量上的部分源碼

編輯:關於Android編程

Android中的Veiw從內存中到呈現在UI界面上需要依次經歷三個階段:量算 -> 布局 -> 繪圖,關於View的量算、布局、繪圖的總體機制可參見博文《 Android中View的布局及繪圖機制》。量算是布局和繪圖的基礎,所以量算是很重要的一個環節。本文將從源碼角度解析View的量算過程,這其中會涉及某些關鍵類以及關鍵方法。

對View進行量算的目的是讓View的父控件知道View想要多大的尺寸。


量算過程概述

如果要進行量算的View是ViewGroup類型,那麼ViewGroup會在onMeasure方法內會遍歷子View依次進行量算,本文重點說明非ViewGroup的View的量算過程,因為我們一旦了解了非ViewGroup的View的量算過程,ViewGroup的量算理解起來就要簡單許多,主要是ViewGroup在其內部對子View再依次執行量算。

  • 整個應用量算的起點是ViewRootImpl類,從它開始依次對子View進行量算,如果子View是一個ViewGroup,那麼又會遍歷該ViewGroup的子View依次進行量算。也就是說,量算會從View樹的根結點,縱向遞歸進行,從而實現自上而下對View樹進行量算,直至完成對葉子節點View的量算。

  • 那麼到底如何對一個View進行量算呢?Android通過調用View的measure()方法對View進行量算,讓該View的父控件知道該View想要多大的尺寸空間。

  • 具體來說,View的父控件ViewGroup會調用View的measure方法,ViewGroup會將一些寬度和高度的限制條件傳遞給View的measure方法。

  • 在View的measure方法會首先從成員變量中讀取以前緩存過的量算結果,如果能找到該緩存值,那麼就基本完事了,如果沒有找到緩存值,那麼measure方法會執行onMeasure回調方法,measure方法會將上述的寬度和高度的限制條件依次傳遞給onMeasure方法。onMeasure方法會完成具體的量算工作,並將量算的結果通過調用View的setMeasuredDimension方法保存到View的成員變量mMeasuredWidth 和mMeasuredHeight中。

  • 量算完成之後,View的父控件就可以通過調用getMeasuredWidth、getMeasuredState、getMeasuredWidthAndState這三個方法獲取View的量算結果。

以上就是非ViewGroup類型的View量算的總體過程。


MeasureSpec簡介

上面我們提到ViewGroup在調用View的measure方法時,會傳入ViewGroup對View的寬度及高度的限制條件,這是合理的,例如ViewGroup的空間有限,它需要告訴子View要量算的尺寸的上限。

上面提到的尺寸的限制條件就是MeasureSpec,它可以通過一個Int類型的值來表示的,該Int值會同時包含兩種信息:mode和size,即模式和尺寸。我們知道Java中Int類型的值是4個字節的,Android會用第一個高位字節存儲mode,然後用剩余的三個字節存儲size。

View有一個靜態內部類MeasureSpec,該類有幾個靜態方法以及靜態常量,我們可以用這些方法將mode和size打包成一個Int值或者是從一個Int值中解析出mode和size。

假設我們已有了一個包含MeasureSpec信息的Int值measureSpec,那麼

  • 通過調用MeasureSpec.getSize(int measureSpec)即可從measureSpec解析出三個字節所包含的尺寸size信息,該方法返回Int類型,也就是說我們得到的size實際上就是對原有的measureSpec的高位字節的8個二進制位都設置為0,該方法的返回值size雖然也是4個字節的Int值,但是已經完全不包含mode信息。

  • 通過調用MeasureSpec.getMode(int measureSpec)即可從measureSpec解析出高位字節所包含的模式mode信息,該方法返回Int類型,也就是說我們得到的mode實際上對原有的measureSpec的低位的三個字節的24個二進制碼都設置為0,該方法的返回值mode雖然也是4個字節的Int值,但是已經完全不包含size信息。

對於尺寸size,我們很好理解,比如表示某個寬度值或者表示某個高度值。那麼mode是什麼呢?

mode的取值有三種,分別是:

  • MeasureSpec.AT_MOST,即0x80000000,該值表示View最大可以取其父ViewGroup給其指定的尺寸,例如現在有個Int值widthMeasureSpec,ViewGroup將其傳遞給了View的measure方法,如果widthMeasureSpec中的mode值是AT_MOST,size是200,那麼表示View能取的最大的寬度是200。

  • MeasureSpec.EXACTLY,即0x40000000,該值表示View必須使用其父ViewGroup指定的尺寸,還是以widthMeasureSpec為例,如果其mode值是EXACTLY,size是200,那麼表示View的寬度必須是200,不多不少才行。

  • MeasureSpec.UNSPECIFIED,即0x00000000,該值表示View的父ViewGroup沒有給View在尺寸上設置限制條件,這種情況下View可以忽略measureSpec中的size,View可以取自己想要的值作為量算的尺寸。

更多信息可參考API文檔 android/view/View.MeasureSpec。


measure方法

measure()的方法簽名是public final void measure(int widthMeasureSpec, int heightMeasureSpec)。

當View的父控件ViewGroup對View進行量算時,會調用View的measure方法,ViewGroup會傳入widthMeasureSpec和heightMeasureSpec,分別表示父控件對View的寬度和高度的一些限制條件。measure方法的源碼如下所示:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) 
{//首先判斷當前View的layoutMode是不是特例LAYOUT_MODE_OPTICAL_BOUNDS    boolean optical = isLayoutModeOptical(this); 
    if (optical != isLayoutModeOptical(mParent)) 
    {  
        //LAYOUT_MODE_OPTICAL_BOUNDS是特例情況,比較少見       
        Insets insets = getOpticalInsets();       
        int oWidth  = insets.left + insets.right;       
        int oHeight = insets.top  + insets.bottom;       
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);       
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);  
    }    
        //根據widthMeasureSpec和heightMeasureSpec計算key值,我們在下面用key值作為鍵,緩存我們量算的結果    
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;    
        //mMeasureCache是LongSparseLongArray類型的成員變量,    
        //其緩存著View在不同widthMeasureSpec、heightMeasureSpec下量算過的結果   
        //如果mMeasureCache為空,我們就新new一個對象賦值給mMeasureCache
        
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); 
    
        //mOldWidthMeasureSpec和mOldHeightMeasureSpec分別表示上次對View進行量算時的widthMeasureSpec和heightMeasureSpec    
        //執行View的measure方法時,View總是先檢查一下是不是真的有必要費很大力氣去做真正的量算工作   
        //mPrivateFlags是一個Int類型的值,其記錄了View的各種狀態位   
        //如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,   
        //那麼表示當前View需要強制進行layout(比如執行了View的forceLayout方法),所以這種情況下要嘗試進行量算    
        //如果新傳入的widthMeasureSpec/heightMeasureSpec與上次量算時的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,  
        //那麼也就是說該View的父ViewGroup對該View的尺寸的限制情況有變化,這種情況下要嘗試進行量算    
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || 
         widthMeasureSpec != mOldWidthMeasureSpec ||  
         heightMeasureSpec != mOldHeightMeasureSpec) 
    { 
        //通過按位操作,重置View的狀態mPrivateFlags,將其標記為未量算狀態       
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;  
        
        //對阿拉伯語、希伯來語等從右到左書寫、布局的語言進行特殊處理 
        resolveRtlPropertiesIfNeeded();  
        
        //在View真正進行量算之前,View還想進一步確認能不能從已有的緩存mMeasureCache中讀取緩存過的量算結果       
        //如果是強制layout導致的量算,那麼將cacheIndex設置為-1,即不從緩存中讀取量算結果       
        //如果不是強制layout導致的量算,那麼我們就用上面根據measureSpec計算出來的key值作為緩存索引cacheIndex。
        
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); 
        
        //sIgnoreMeasureCache是一個boolean類型的成員變量,其值是在View的構造函數中計算的,而且只計算一次   
        //一些老的App希望在一次layou過程中,onMeasure方法總是被調用,       
        //具體來說其值是通過如下計算的:
        sIgnoreMeasureCache = targetSdkVersion < KITKAT;      
        //也就是說如果targetSdkVersion的API版本低於KITKAT,即API level小於19,那麼sIgnoreMeasureCache為true    
        
        if (cacheIndex < 0 || sIgnoreMeasureCache)
        {   
        
            //如果運行到此處,表示我們沒有從緩存中找到量算過的尺寸或者是sIgnoreMeasureCache為true導致我們要忽略緩存結果         
            //此處調用onMeasure方法,並把尺寸限制條件widthMeasureSpec和heightMeasureSpec傳入進去         
            //onMeasure方法中將會進行實際的量算工作,並把量算的結果保存到成員變量中 
            
            onMeasure(widthMeasureSpec, heightMeasureSpec); 
            
            //onMeasure執行完後,通過位操作,重置View的狀態mPrivateFlags,將其標記為在layout之前不必再進行量算的狀態      
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;       
        } else { 
        
            //如果運行到此處,那麼表示當前的條件允許View從緩存成員變量mMeasureCache中讀取量算過的結果          
            //用上面得到的cacheIndex從緩存mMeasureCache中取出值,不必在調用onMeasure方法進行量算了           
            long value = mMeasureCache.valueAt(cacheIndex);           
            //一旦我們從緩存中讀到值,我們就可以調用setMeasuredDimensionRaw方法將當前量算的結果到成員變量中       
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);      
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;        
        }       
            //如果我們自定義的View重寫了onMeasure方法,但是沒有調用setMeasuredDimension()方法, 
            //那麼此處就會拋出異常,提醒開發者在onMeasure方法中調用setMeasuredDimension()方法    
            //Android是如何知道我們有沒有在onMeasure方法中調用setMeasuredDimension()方法的呢?    
            //方法很簡單,還是通過解析狀態位mPrivateFlags。       


            //setMeasuredDimension()方法中會將mPrivateFlags設置為PFLAG_MEASURED_DIMENSION_SET狀態,即已量算狀態,   
            //此處就檢查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET狀態即可判斷setMeasuredDimension是否被調用  
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) 
        {   
            throw new IllegalStateException("View with id " + getId() + ": "   + getClass().getName() + "#onMeasure() did not set the" + 
            " measured dimension by calling"  + " setMeasuredDimension()");      
        }       
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;   
    }    
    //mOldWidthMeasureSpec和mOldHeightMeasureSpec保存著最近一次量算時的MeasureSpec,   
    //在量算完成後將這次新傳入的MeasureSpec賦值給它們   
    mOldWidthMeasureSpec = widthMeasureSpec;   
    mOldHeightMeasureSpec = heightMeasureSpec;   
    //最後用上面計算出的key作為鍵,量算結果作為值,將該鍵值對放入成員變量mMeasureCache中, 
    //這樣就實現了對本次量算結果的緩存,以便在下次measure方法執行的時候,有可能將其從中直接讀出, 
    //從而省去實際量算的步驟  
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |  (long) mMeasuredHeight & 0xffffffffL);
}

上面的注釋對每行代碼都進行了詳細的說明,如果大家仔細讀了的話,相信能一目了然,這裡根據上面的注釋簡單總結一下measure方法都干了什麼事:

  • 首先,我們要知道並不是只要View的measure方法執行的時候View就一定要傻傻的真的去做量算工作,View也喜歡偷懶,如果View發現沒有必要去量算的話,那它就不會真的去做量算的工作。

  • 具體來說,View先查看是不是要強制量算以及這次measure中傳入的MeasureSpec與上次量算的MeasureSpec是否相同,如果不是強制量算或者MeasureSpec與上次的量算的MeasureSpec相同,那麼View就不必真的去量算了。

  • 如果不滿足上述條件,View就考慮去做量算工作。但是在量算之前,View還想偷懶,它會以MeasureSpec計算出的key值作為鍵,去成員變量mMeasureCache中查找是否緩存過對應key的量算結果,如果能找到,那麼就簡單調用一下setMeasuredDimensionRaw方法,將從緩存中讀到的量算結果保存到成員變量mMeasuredWidth和mMeasuredHeight中。

  • 如果不能從mMeasureCache中讀到緩存過的量算結果,那麼這次View就真的不能再偷懶了,只能乖乖地調用onMeasure方法去完成實際的量算工作,並且將尺寸限制條件widthMeasureSpec和heightMeasureSpec傳遞給onMeasure方法。關於onMeasure方法,我們會在下面詳細介紹。

  • 不論上面代碼走了哪個判斷的分支,最終View都會得到量算的結果,並且將結果緩存到成員變量mMeasureCache中,以便下次執行measure方法時能夠從其中讀取緩存值。

  • 需要說明的是,View有一個成員變量mPrivateFlags,用以保存View的各種狀態位,在量算開始前,會將其設置為未量算狀態,在量算完成後會將其設置為已量算狀態。

onMeasure方法

我們在上面提到,當View在measure方法中發現不得不進行實際的量算工作時,將會調用onMeasure方法,並且將尺寸限制條件widthMeasureSpec和heightMeasureSpec作為參數傳遞給onMeasure方法。View的onMeasure方法不是空方法,它提供了一個默認的具體實現。
onMeasure方法的代碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
{ 
    //onMeasure調用了setMeasuredDimension方法,  
    //setMeasuredDimension又需要調用getDefaultSize方法, 
    //getDefaultSize又需要調用getSuggestedMinimumWidth和getSuggestedMinimumHeight方法  
    
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),       
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

我們發現onMeasure方法中會調用setMeasuredDimension方法,setMeasuredDimension又需要調用getDefaultSize方法,getDefaultSize又需要調用getSuggestedMinimumWidth和getSuggestedMinimumHeight方法,即
setMeasuredDimension -> getDefaultSize -> getSuggestedMinimumWidth/Height

那我們就先研究getSuggestedMinimumWidth/Height,然後再依次研究getDefaultSize和setMeasuredDimension,這樣就能把onMeasure方法搞明白了。其實getSuggestedMinimumWidth和getSuggestedMinimumHeight的實現邏輯基本一樣,我們此處只研究getSuggestedMinimumWidth方法即可。


getSuggestedMinimumWidth方法

getSuggestedMinimumWidth用於返回View推薦的最小寬度,其代碼如下所示:

protected int getSuggestedMinimumWidth() {  
    //如果沒有給View設置背景,那麼就返回View本身的最小寬度mMinWidth   
    //如果給View設置了背景,那麼就取View本身最小寬度mMinWidth和背景的最小寬度的最大值  
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
  • 如果沒有給View設置背景,那麼就返回View本身的最小寬度mMinWidth

  • 如果給View設置了背景,那麼就取View本身最小寬度mMinWidth和背景的最小寬度的最大值

那你可能有疑問,View中保存的最小寬度mMinWidth的值是從哪來的呢?實際上有兩種辦法給View設置最小寬度。

  • 第一種情況是,mMinWidth是在View的構造函數中被賦值的,View通過讀取XML中定義的minWidth的值來設置View的最小寬度mMinWidth,以下代碼片段是View構造函數中解析minWidth的部分:
//遍歷到XML中定義的minWith屬性case R.styleable.View_minWidth:
//讀取XML中定義的屬性值作為mMinWidth,如果XML中未定義,則設置為0mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
第二種情況是調用View的setMinimumWidth方法給View的最小寬度mMinWidth賦值,setMinimumWidth方法的代碼如下所示:

 

public void setMinimumWidth(int minWidth) 
{
    mMinWidth = minWidth;   
    requestLayout();
}

 

這樣我們就搞明白了getSuggestedMinimumWidth方法是怎麼執行的了,getSuggestedMinimumHeight方法與其邏輯完全一致,只不過是把寬度換成了高度,在此就不再贅述了。


getDefaultSize

我們在onMeasure方法中發現,onMeasure會執行以下兩行代碼:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

我們已經研究了getSuggestedMinimumWidth/Height,知道其會返回View的最小寬度和高度,現在我們開始研究getDefaultSize方法。

Android會將View想要的尺寸以及其父控件對其尺寸限制信息measureSpec傳遞給getDefaultSize方法,該方法要根據這些綜合信息計算最終的量算的尺寸。

其源碼如下所示:

public static int getDefaultSize(int size, int measureSpec) 
{    
    //size表示的是View想要的尺寸信息,比如最小寬度或最小高度   
    int result = size;   
    //從measureSpec中解析出specMode信息   
    int specMode = MeasureSpec.getMode(measureSpec);   
    //從measureSpec中解析出specSize信息,不要將specSize與上面的size變量搞混   
    int specSize = MeasureSpec.getSize(measureSpec);  
    
    switch (specMode) 
    {  
    
        //如果mode是UNSPECIFIED,表示View的父ViewGroup沒有給View在尺寸上設置限制條件    
        case MeasureSpec.UNSPECIFIED:     
        
        //此處當mode是UNSPECIFIED時,View就直接用自己想要的尺寸size作為量算的結果      
        result = size;        
        break; 
        
        //如果mode是UNSPECIFIED,那麼表示View最大可以取其父ViewGroup給其指定的尺寸    
        //如果mode是EXACTLY,那麼表示View必須使用其父ViewGroup指定的尺寸   
        case MeasureSpec.AT_MOST:  
        
        case MeasureSpec.EXACTLY:   
        //此處mode是UNSPECIFIED或EXACTLY時,View就用其父ViewGroup指定的尺寸作為量算的結果       
        result = specSize;           
        break;   
    }    
    return result;
}

通過以上代碼,我們就會發現View的父ViewGroup傳遞給View的限制條件measureSpec的作用在該方法中體現的淋漓盡致。

  • 首先根據measuredSpec解析出對應的specMode和specSize

  • 當mode是UNSPECIFIED時,View就直接用自己想要的尺寸size作為量算的結果

  • 當mode是UNSPECIFIED或EXACTLY時,View就用其父ViewGroup指定的尺寸作為量算的結果

最終,View會根據measuredSpec限制條件,得到最終的量算的尺寸。

這樣在onMeasure方法中,
當執行getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)時,我們就得到了最終量算到的寬度值;
當執行getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)時,我們就得到了最終量算到的高度值。


setMeasuredDimension

在前面我們研究onMeasure方法時就已經看到setMeasuredDimension會調用getDefaultSize方法,會將已經量算到的寬度值和高度值作為參數傳遞給setMeasuredDimension方法,我們研究一下該方法。

其源碼如下所示:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) 
{  
    boolean optical = isLayoutModeOptical(this);    
    if (optical != isLayoutModeOptical(mParent)) 
    {       
        //layoutMode是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情況,我們不考慮        
        Insets insets = getOpticalInsets();        
        int opticalWidth  = insets.left + insets.right;   
        int opticalHeight = insets.top  + insets.bottom;       
        measuredWidth  += optical ? opticalWidth  : -opticalWidth;       
        measuredHeight += optical ? opticalHeight : -opticalHeight;  
    }    
    //最終調用setMeasuredDimensionRaw方法,將量算結果傳入進去    
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

該方法會在開始判斷layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情況,這種特例很少見,我們直接忽略掉。

setMeasuredDimension方法最後將量算的結果傳遞給方法setMeasuredDimensionRaw,我們再研究一下setMeasuredDimensionRaw這方法。


setMeasuredDimensionRaw

setMeasuredDimensionRaw接收兩個參數,分別是已經量算完成的寬度和高度。

其源碼如下所示:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)
{    
    //將量算完成的寬度measuredWidth保存到View的成員變量mMeasuredWidth中   
    mMeasuredWidth = measuredWidth;    
    //將量算完成的高度measuredHeight保存到View的成員變量mMeasuredHeight中   
    mMeasuredHeight = measuredHeight;    
    
    //最後將View的狀態位mPrivateFlags設置為已量算狀態    
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

我們發現,在該方法中做了三件事:

  • 將量算完成的寬度measuredWidth保存到View的成員變量mMeasuredWidth中

  • 將量算完成的高度measuredHeight保存到View的成員變量mMeasuredHeight中

  • 最後將View的狀態位mPrivateFlags設置為已量算狀態

量算完成的尺寸的state

至此,View的量算過程就完成了,但是View的父ViewGroup如何讀取到View量算的結果呢?

為此,View提供了三組方法,分別是:
1. getMeasuredWidth和getMeasuredHeight方法
2. getMeasuredWidthAndState和getMeasuredHeightAndState方法
3. getMeasuredState方法

有些人可能會納悶,只要有了第一組方法不就行了嗎?後面那兩組方法有啥用?

此處我們要再仔細研究一下View中保存量算結果的成員變量mMeasuredWidth和mMeasuredHeight,下面的討論我們都只討論寬度,理解了寬度的處理方式,高度也是完全一樣的。

mMeasuredWidth是一個Int類型的值,其是由4個字節組成的。

我們先假設mMeasuredWidth只存儲了量算完成的寬度信息,而且View的父ViewGroup可以通過相關方法得到該值。但是存在這樣一種情況:View在量算時,父ViewGroup給其傳遞的widthMeasureSpec中的specMode的值是AT_MOST,specSize是100,但是View的最小寬度是200,顯然父ViewGroup指定的specSize不能滿足View的大小,但是由於specMode的值是AT_MOST,View在getDefaultSize方法中不得不妥協,只能含淚將量算的最終寬度設置為100。然後其父ViewGroup通過某些方法獲取到該View的量算寬度為100時,ViewGroup以為子View只需要100就夠了,最終給了子View寬度為100的空間,這就導致了在UI界面上View特別窄,用戶體驗也就不好。

Android為讓其View的父控件獲取更多的信息,就在mMeasuredWidth上下了很大功夫,雖然是一個Int值,但是想讓它存儲更多信息,具體來說就是把mMeasuredWidth分成兩部分:

  • 其高位的第一個字節為第一部分,用於標記量算完的尺寸是不是達到了View想要的寬度,我們稱該信息為量算的state信息。
  • 其低位的三個字節為第二部分,用於存儲實際的量算到的寬度。

由此我們可以看出Android真是物盡其用,一個變量能包含兩個信息,這個有點類似於measureSpec的道理,但是二者又有不同:

  • measureSpec是將限制條件mode從ViewGroup傳遞給其子View。
  • mMeasuredWidth、mMeasuredHeight是將帶有量算結果的state標志位信息從View傳遞給其父ViewGroup。

那麼你可能會問,在本文中我們沒看到對mMeasuredWidth的高位字節進行特殊處理啊?我們下面看一下View中的resolveSizeAndState方法。


resolveSizeAndState

resolveSizeAndState方法與getDefaultSize方法類似,其內部實現的邏輯是一樣的,但是又有區別,getDefaultSize僅僅返回最終量算的尺寸信息,但resolveSizeAndState除了返回最終尺寸信息還會有可能返回量算的state標志位信息。

resolveSizeAndState方法的源碼如下所示:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState)
{    
    final int specMode = MeasureSpec.getMode(measureSpec);    
    final int specSize = MeasureSpec.getSize(measureSpec);    
    final int result; 
    
    switch (specMode) 
    {        
    case MeasureSpec.AT_MOST:            
    if (specSize < size) 
        {                
        //當specMode為AT_MOST,並且父控件指定的尺寸specSize小於View自己想要的尺寸時,               
        //我們就會用掩碼MEASURED_STATE_TOO_SMALL向量算結果加入尺寸太小的標記                
        //這樣其父ViewGroup就可以通過該標記其給子View的尺寸太小了,                
        //然後可能分配更大一點的尺寸給子View                
        result = specSize | MEASURED_STATE_TOO_SMALL;            
        } else {                
        result = size;            
        }           
    break;        
    case MeasureSpec.EXACTLY:            
    result = specSize;          
    break;        
    case MeasureSpec.UNSPECIFIED:        
    default:           
    result = size;    
    }   
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

當specMode為AT_MOST,並且父控件指定的尺寸specSize小於View自己想要的尺寸時,我們就會用掩碼MEASURED_STATE_TOO_SMALL向量算結果加入尺寸太小的標記,這樣其父ViewGroup就可以通過該標記其給子View的尺寸太小了,然後可能分配更大一點的尺寸給子View。

getDefaultSize方法只是onMeasure方法中獲取最終尺寸的默認實現,其返回的信息比resolveSizeAndState要少,那麼什麼時候才會調用resolveSizeAndState方法呢? 主要有兩種情況:

  • Android中的許多layout類都調用了resolveSizeAndState方法,比如LinearLayout在量算過程中會調用resolveSizeAndState方法而非getDefaultSize方法。
  • 我們自己在實現自定義的View或ViewGroup時,我們可以重寫onMeasure方法,並在該方法內調用resolveSizeAndState方法。

getMeasuredXXX系列方法

現在我們再回過頭來看以下三組方法:

  • getMeasuredWidth和getMeasuredHeight方法

該組方法只返回量算結果中的的尺寸信息,去掉了高位字節的state信息,以getMeasuredWidth方法為例,其源碼如下:

public final int getMeasuredWidth() 
{    
    //MEASURED_SIZE_MASK的值為0x00ffffff,用mMeasuredWidth與掩碼MEASURED_SIZE_MASK進行按位與運算, 
    //可以將返回值中的高位字節的8個bit位全置為0,從而去掉了高位字節的state信息    
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}
  • MEASURED_SIZE_MASK的值為0x00ffffff,用mMeasuredWidth與掩碼MEASURED_SIZE_MASK進行按位與運算,可以將返回值中的高位字節的8個bit位全置為0,從而去掉了高位字節的state信息
  • getMeasuredWidthAndState和getMeasuredHeightAndState方法

該組方法返回的量算結果中同時包含尺寸和state信息(如果state存在的話),以getMeasuredWidthAndState方法為例,其源碼如下所示:

public final int getMeasuredWidthAndState() 
{   
    //該方法直接返回成員變量mMeasuredWidth,因為mMeasuredWidth本身已經包含了尺寸以及可能的state信息    
    return mMeasuredWidth;
}
  • 該方法直接返回成員變量mMeasuredWidth,因為mMeasuredWidth本身已經包含了尺寸以及可能的state信息

  • getMeasuredState方法

該方法返回的Int值中同時包含寬度量算的state以及高度量算的state,不包含任何的尺寸信息,其源碼如下所示:

public final int getMeasuredState() 
{    
    //將寬度量算的state存儲在Int值的第一個字節中,即高位首字節   
    //將高度量算的state存儲在Int值的第三個字節中   
    return (mMeasuredWidth&MEASURED_STATE_MASK) | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT) & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
  • 我們簡單分析一下以上代碼:

  • 掩碼MEASURED_STATE_MASK的值為常量0xff000000,其高位字節的8個bit位全為1,剩余低位字節的三個字節的24個bit位全為0

  • MEASURED_HEIGHT_STATE_SHIFT的值為常量16

  • 當執行(mMeasuredWidth&MEASURED_STATE_MASK)時,將mMeasuredWidth與MEASURED_STATE_MASK進行按位與操作,該表達式的值高位字節保留了量算後寬度的state,過濾掉了其低位三個字節所存儲的寬度size

  • 由於我們已經用高位首字節存儲了量算後寬度的state,所以高度的state就不能存儲在高位首字節了。Android打算把它存儲在第三個字節中。(mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)表示將mMeasuredHeight向右移16位,這樣高度的state字節就從原來的第一個字節右移動到了第三個字節,由於高度的state向右移動了,所以其對應的掩碼也有相應移動。(MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)表示state的掩碼也從第一個字節右移16位到了第三個字節,即掩碼從0xff000000變成了0x0000ff00。然後用右移後的state與右移後的掩碼執行按位與操作,這樣就在第三個字節保留了高度的state信息,並且過濾掉了第1、2、4字節中的信息,即將這三個字節中的24個bit位置為0。

  • 最後,將我們得到的寬度的state與高度的state進行按位或操作,這樣就將寬度和高度的state都保存在一個Int值中:第一個字節存儲寬度的state,第三個字節存儲高度的state。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved