Android源碼完全解析——View的Measure過程

在Android中,Veiw從內(nèi)存中到呈現(xiàn)在UI界面上需要經(jīng)過measure(測(cè)量)、layout(布局)、draw(繪制)這樣一個(gè)過程。為什么需要measure過程?因?yàn)樵贏ndroid中View有自適應(yīng)尺寸的機(jī)制,在用自適應(yīng)尺寸來(lái)定義View大小的時(shí)候,View的真實(shí)尺寸還不能確定,這時(shí)候就需要根據(jù)View的寬高匹配規(guī)則,經(jīng)過計(jì)算,得到具體的像素值,measure過程就是干這件事。
本文將從源碼角度解析View的measure過程,這其中會(huì)涉及某些關(guān)鍵類以及關(guān)鍵方法。

MeasureSpec

MeasureSpec封裝了父布局傳遞給子布局的布局要求,它通過一個(gè)32位int類型的值來(lái)表示,該值包含了兩種信息,高兩位表示的是SpecMode(測(cè)量模式),低30位表示的是SpecSize(測(cè)量的具體大小)。下面通過注釋的方式來(lái)分析來(lái)類:

/**  
 * 三種SpecMode: 
 * 1.UNSPECIFIED 
 * 父ViewGroup沒有對(duì)子View施加任何約束,子view可以是任意大小。這種情況比較少見,主要用于系統(tǒng)內(nèi)部多次measure的情形,用到的一般都是可以滾動(dòng)的容器中的子view,比如ListView、GridView、RecyclerView中某些情況下的子view就是這種模式。一般來(lái)說(shuō),我們不需要關(guān)注此模式。
 * 2.EXACTLY 
 * 該view必須使用父ViewGroup給其指定的尺寸。對(duì)應(yīng)match_parent或者具體數(shù)值(比如30dp)
 * 3.AT_MOST 
 * 該View最大可以取父ViewGroup給其指定的尺寸。對(duì)應(yīng)wrap_content
 *  
 * MeasureSpec使用了二進(jìn)制去減少對(duì)象的分配。 
 */  
public class MeasureSpec {  
        // 進(jìn)位大小為2的30次方(int的大小為32位,所以進(jìn)位30位就是要使用int的最高位和第二高位也就是32和31位做標(biāo)志位)  
        private static final int MODE_SHIFT = 30;  
          
        // 運(yùn)算遮罩,0x3為16進(jìn)制,10進(jìn)制為3,二進(jìn)制為11。3向左進(jìn)位30,就是11 00000000000(11后跟30個(gè)0)  
        // (遮罩的作用是用1標(biāo)注需要的值,0標(biāo)注不要的值。因?yàn)?與任何數(shù)做與運(yùn)算都得任何數(shù),0與任何數(shù)做與運(yùn)算都得0)  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
  
        // 0向左進(jìn)位30,就是00 00000000000(00后跟30個(gè)0)  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        // 1向左進(jìn)位30,就是01 00000000000(01后跟30個(gè)0)  
        public static final int EXACTLY     = 1 << MODE_SHIFT;  
        // 2向左進(jìn)位30,就是10 00000000000(10后跟30個(gè)0)  
        public static final int AT_MOST     = 2 << MODE_SHIFT;  
  
        /** 
         * 根據(jù)提供的size和mode得到一個(gè)詳細(xì)的測(cè)量結(jié)果 
         */  
        // 第一個(gè)return:
        // measureSpec = size + mode;   (注意:二進(jìn)制的加法,不是十進(jìn)制的加法!)  
        // 這里設(shè)計(jì)的目的就是使用一個(gè)32位的二進(jìn)制數(shù),32和31位代表了mode的值,后30位代表size的值  
        // 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100  
        // 
        // 第二個(gè)return:
        // size & ~MODE_MASK就是取size 的后30位,mode & MODE_MASK就是取mode的前兩位,最后執(zhí)行或運(yùn)算,得出來(lái)的數(shù)字,前面2位包含代表mode,后面30位代表size
        public static int makeMeasureSpec(int size, int mode) {  
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }  
  
        /** 
         * 獲得SpecMode
         */  
        // mode = measureSpec & MODE_MASK;  
        // MODE_MASK = 11 00000000000(11后跟30個(gè)0),原理是用MODE_MASK后30位的0替換掉measureSpec后30位中的1,再保留32和31位的mode值。  
        // 例如10 00..00100 & 11 00..00(11后跟30個(gè)0) = 10 00..00(AT_MOST),這樣就得到了mode的值  
        public static int getMode(int measureSpec) {  
            return (measureSpec & MODE_MASK);  
        }  
  
        /** 
         * 獲得SpecSize 
         */  
        // size = measureSpec & ~MODE_MASK;  
        // 原理同上,不過這次是將MODE_MASK取反,也就是變成了00 111111(00后跟30個(gè)1),將32,31替換成0也就是去掉mode,保留后30位的size  
        public static int getSize(int measureSpec) {  
            return (measureSpec & ~MODE_MASK);  
        }  
}  

measure()

當(dāng)View的父ViewGroup對(duì)View進(jìn)行測(cè)量時(shí),會(huì)調(diào)用View的measure方法,ViewGroup會(huì)傳入widthMeasureSpecheightMeasureSpec,分別表示父控件對(duì)View的寬度和高度的一些限制條件。源碼分析該方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //首先判斷當(dāng)前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);
        }

        //根據(jù)widthMeasureSpec和heightMeasureSpec計(jì)算key值,在下面用key值作為鍵,緩存我們測(cè)量得到的結(jié)果
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        //mMeasureCache是LongSparseLongArray類型的成員變量,
        //其緩存著View在不同widthMeasureSpec、heightMeasureSpec下測(cè)量過的結(jié)果
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        //mOldWidthMeasureSpec和mOldHeightMeasureSpec分別表示上次對(duì)View進(jìn)行測(cè)量時(shí)的widthMeasureSpec和heightMeasureSpec
        //執(zhí)行View的measure方法時(shí),View總是先檢查一下是不是真的有必要費(fèi)很大力氣去做真正的測(cè)量工作
        //mPrivateFlags是一個(gè)Int類型的值,其記錄了View的各種狀態(tài)位
        //如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
        //那么表示當(dāng)前View需要強(qiáng)制進(jìn)行l(wèi)ayout(比如執(zhí)行了View的forceLayout方法),所以這種情況下要嘗試進(jìn)行測(cè)量
        //如果新傳入的widthMeasureSpec/heightMeasureSpec與上次測(cè)量時(shí)的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
        //那么也就是說(shuō)該View的父ViewGroup對(duì)該View的尺寸的限制情況有變化,這種情況下要嘗試進(jìn)行測(cè)量
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            //通過按位操作,重置View的狀態(tài)標(biāo)志mPrivateFlags,將其標(biāo)記為未測(cè)量狀態(tài)
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            
            //對(duì)阿拉伯語(yǔ)、希伯來(lái)語(yǔ)等從右到左書寫、布局的語(yǔ)言進(jìn)行特殊處理
            resolveRtlPropertiesIfNeeded();

        //在View真正進(jìn)行測(cè)量之前,View還想進(jìn)一步確認(rèn)能不能從已有的緩存mMeasureCache中讀取緩存過的測(cè)量結(jié)果
        //如果是強(qiáng)制layout導(dǎo)致的測(cè)量,那么將cacheIndex設(shè)置為-1,即不從緩存中讀取測(cè)量結(jié)果
        //如果不是強(qiáng)制layout導(dǎo)致的測(cè)量,那么我們就用上面根據(jù)measureSpec計(jì)算出來(lái)的key值作為緩存索引cacheIndex,這時(shí)候有可能找到相應(yīng)的值,找到就返回對(duì)應(yīng)索引;也可能找不到,找不到就返回-1
            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);

            if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //在緩存中找不到相應(yīng)的值或者需要忽略緩存結(jié)果的時(shí)候,重新測(cè)量一次
            //此處調(diào)用onMeasure方法,并把尺寸限制條件widthMeasureSpec和heightMeasureSpec傳入進(jìn)去
            //onMeasure方法中將會(huì)進(jìn)行實(shí)際的測(cè)量工作,并把測(cè)量的結(jié)果保存到成員變量中
                onMeasure(widthMeasureSpec, heightMeasureSpec);
            //onMeasure執(zhí)行完后,通過位操作,重置View的狀態(tài)mPrivateFlags,將其標(biāo)記為在layout之前不必再進(jìn)行測(cè)量的狀態(tài)
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
            //如果運(yùn)行到此處,那么表示當(dāng)前的條件允許View從緩存成員變量mMeasureCache中讀取測(cè)量過的結(jié)果
            //用上面得到的cacheIndex從緩存mMeasureCache中取出值,不必在調(diào)用onMeasure方法進(jìn)行測(cè)量了
                long value = mMeasureCache.valueAt(cacheIndex);
            //一旦我們從緩存中讀到值,我們就可以調(diào)用setMeasuredDimensionRaw方法將當(dāng)前測(cè)量的結(jié)果保存到成員變量中
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        //如果我們自定義的View重寫了onMeasure方法,但是沒有調(diào)用setMeasuredDimension()方法,
        //那么此處就會(huì)拋出異常,提醒開發(fā)者在onMeasure方法中調(diào)用setMeasuredDimension()方法
        //Android是如何知道我們有沒有在onMeasure方法中調(diào)用setMeasuredDimension()方法的呢?
        //方法很簡(jiǎn)單,還是通過解析狀態(tài)位mPrivateFlags。
        //setMeasuredDimension()方法中會(huì)將mPrivateFlags設(shè)置為PFLAG_MEASURED_DIMENSION_SET狀態(tài),即已測(cè)量狀態(tài),
        //此處就檢查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET狀態(tài)即可判斷setMeasuredDimension是否被調(diào)用
            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()");
            }
        //到了這里,View已經(jīng)測(cè)量完了并且將測(cè)量的結(jié)果保存在View的mMeasuredWidth和mMeasuredHeight中,將標(biāo)志位置為可以layout的狀態(tài)
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
    //mOldWidthMeasureSpec和mOldHeightMeasureSpec保存著最近一次測(cè)量時(shí)的MeasureSpec,
    //在測(cè)量完成后將這次新傳入的MeasureSpec賦值給它們
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    //最后用上面計(jì)算出的key作為鍵,測(cè)量結(jié)果作為值,將該鍵值對(duì)放入成員變量mMeasureCache中,
    //這樣就實(shí)現(xiàn)了對(duì)本次測(cè)量結(jié)果的緩存,以便在下次measure方法執(zhí)行的時(shí)候,有可能將其從中直接讀出,
    //從而省去實(shí)際測(cè)量的步驟
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

上面的注釋已經(jīng)一目了然,這里再總結(jié)一下measure()都干了什么事:

  • 首先,調(diào)用View.measure()方法時(shí)View并不是立即就去測(cè)量,而是先判斷一下是否有必要進(jìn)行測(cè)量操作,如果不是強(qiáng)制測(cè)量或者MeasureSpec與上次的MeasureSpec相同的時(shí)候,那么View就不需要重新測(cè)量了.
  • 如果不滿足上面條件,View就考慮去做測(cè)量工作了.但在測(cè)量之前,View還想偷懶,如果能在緩存中找到上次的測(cè)量結(jié)果,那直接從緩存中獲取就可以了.它會(huì)以MeasureSpec計(jì)算出的key值作為鍵,去成員變量mMeasureCache中查找是否緩存過對(duì)應(yīng)key的測(cè)量結(jié)果,如果能找到,那么就簡(jiǎn)單調(diào)用一下setMeasuredDimensionRaw方法,將從緩存中讀到的測(cè)量結(jié)果保存到成員變量mMeasuredWidthmMeasuredHeight中。
  • 如果不能從mMeasureCache中讀到緩存過的測(cè)量結(jié)果,那么這次View就真的不能再偷懶了,只能乖乖地調(diào)用onMeasure()方法去完成實(shí)際的測(cè)量工作,并且將尺寸限制條件widthMeasureSpecheightMeasureSpec傳遞給onMeasure()方法。關(guān)于onMeasure()方法,我們會(huì)在下面詳細(xì)介紹。
  • 不論上面代碼走了哪個(gè)判斷的分支,最終View都會(huì)得到測(cè)量的結(jié)果,并且將結(jié)果保存到mMeasuredWidthmMeasuredHeight這兩個(gè)成員變量中,同時(shí)緩存到成員變量mMeasureCache中,以便下次執(zhí)行measure()方法時(shí)能夠從其中讀取緩存值。
  • 需要說(shuō)明的是,View有一個(gè)成員變量mPrivateFlags,用以保存View的各種狀態(tài)位,在測(cè)量開始前,會(huì)將其設(shè)置為未測(cè)量狀態(tài),在測(cè)量完成后會(huì)將其設(shè)置為已測(cè)量狀態(tài)。

onMeasure()

上面我們提到,View的measure()方法在需要進(jìn)行實(shí)際的測(cè)量工作時(shí)會(huì)調(diào)用onMeasure()方法.看下源碼:

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

我們發(fā)現(xiàn)onMeasure()方法中調(diào)用了setMeasuredDimension()方法,setMeasuredDimension()又調(diào)用了getDefaultSize()方法.getDefaultSize()又調(diào)用了getSuggestedMinimumWidth()getSuggestedMinimumHeight(),那我們反向研究一下,先看下getSuggestedMinimumWidth()方法(getSuggestedMinimumHeight()原理getSuggestedMinimumWidth()跟一樣).

getSuggestedMinimumWidth()

該方法返回View推薦的最小寬度,源碼如下:

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

源碼很簡(jiǎn)單,如果View沒有背景,就直接返回View本身的最小寬度mMinWidth;如果給View設(shè)置了背景,就取View本身的最小寬度mMinWidth和背景的最小寬度的最大值.

那么mMinWidth是哪里來(lái)的?搜索下源碼就可以知道,View的最小寬度mMinWidth可以有兩種方式進(jìn)行設(shè)置:

  • 第一種是在View的構(gòu)造方法中進(jìn)行賦值的,View通過讀取XML文件中View設(shè)置的minWidth屬性來(lái)為mMinWidth賦值:

    case R.styleable.View_minWidth:
         mMinWidth = a.getDimensionPixelSize(attr, 0);
         break;
    
  • 第二種是在調(diào)用View的setMinimumWidth方法為mMinWidth賦值:

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

getDefaultSize()

知道了getSuggestedMinimumWidth()/getSuggestedMinimumHeight()這兩個(gè)方法返回的是View的最小寬度/高度之后,我們將得到的最小寬度/高度值作為參數(shù)傳給getDefaultSize(int size, int measureSpec)方法,看下源碼:

    public static int getDefaultSize(int size, int measureSpec) {
      //size是傳進(jìn)來(lái)的View自己想要的最小寬度/高度
        int result = size;
      //measureSpec是父ViewGroup給View的限制條件,解析出specMode和specSize
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
       //如果specMode為UNSPECIFIED,就表明父ViewGroup沒有對(duì)該View尺寸進(jìn)行限制,直接取View自己想要的寬高
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        //如果specMode為EXACTLY,表明父ViewGroup要求該View必須用父ViewGroup指定的尺寸(specSize),取父ViewGroup指定的寬高值
        //如果specMode為AT_MOST,表明ViewGroup給該View指定了最大寬度/高度尺寸(specSize),取父ViewGroup指定的最大寬度/高度。
        //這里肯定有人有疑問了?為什么specMode為AT_MOST是取View能到達(dá)的最大寬高值specSize,跟EXACTLY模式下的取值一模一樣,聯(lián)想到EXACTLY對(duì)應(yīng)match_parent,AT_MOST對(duì)應(yīng)wrap_content,那這樣wrap_content不就跟match_parent一樣的效果么?是的,調(diào)用這個(gè)方法在測(cè)量的時(shí)候,wrap_content確實(shí)跟match_parent一樣的效果,這樣做有可能是Android還沒適配wrap_content而做的簡(jiǎn)單處理,就像Recyclerview早期的版本就沒有適配wrap_content,導(dǎo)致wrap_content和match_parent一樣的效果,直到23.2.0版本才將match_parent和wrap_content區(qū)分開來(lái)。那適配了wrap_content的測(cè)量方法在哪里呢?下文會(huì)講到。
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

通過代碼可以看到,父ViewGroup通過measureSpec對(duì)View尺寸的限制作用已經(jīng)體現(xiàn)出來(lái)了。最終通過該方法可以得到View在符合ViewGroup的限制條件下的默認(rèn)尺寸,即

getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec):獲得該View在符合ViewGroup的限制條件下的默認(rèn)寬度值

getDefaultSize(getSuggestedMinimumHeight(), widthMeasureSpec):獲得該View在符合ViewGroup的限制條件下的默認(rèn)高度值

從注釋可以看出,getDefaultSize()這個(gè)測(cè)量方法并沒有適配wrap_content這一種布局模式,只是簡(jiǎn)單地將wrap_contentmatch_parent等同起來(lái)。

到了這里,我們要注意一個(gè)問題,getDefaultSize()方法中wrap_contentmatch_parent屬性的效果是一樣的,而該方法是View的onMeasure()中默認(rèn)調(diào)用的,也就是說(shuō),對(duì)于一個(gè)直接繼承自View的自定義View來(lái)說(shuō),它的wrap_content和match_parent屬性是一樣的效果,因此如果要實(shí)現(xiàn)自定義View的wrap_content,則要重寫onMeasure()方法,對(duì)wrap_content屬性進(jìn)行處理。如何處理呢?也很簡(jiǎn)單,代碼如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  //取得父ViewGroup指定的寬高測(cè)量模式和尺寸
  int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
  int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    //如果寬高都是AT_MOST的話,即都是wrap_content布局模式,就用View自己想要的寬高值
        setMeasuredDimension(mWidth, mHeight);
  }else if (widthSpecMode == MeasureSpec.AT_MOST) {
    //如果只有寬度都是AT_MOST的話,即只有寬度是wrap_content布局模式,寬度就用View自己想要的寬度值,高度就用父ViewGroup指定的高度值
        setMeasuredDimension(mWidth, heightSpecSize);
  }else if (heightSpecMode == MeasureSpec.AT_MOST) {
    //如果只有高度都是AT_MOST的話,即只有高度是wrap_content布局模式,高度就用View自己想要的寬度值,寬度就用父ViewGroup指定的高度值
        setMeasuredDimension(widthSpecSize, mHeight);
  }
}

在上面的代碼中,我們要給View指定一個(gè)默認(rèn)的內(nèi)部寬/高(mWidthmHeight),并在wrap_content時(shí)設(shè)置此寬/高即可。

setMeasuredDimension()

現(xiàn)在再來(lái)看下setMeasuredDimension()這個(gè)方法,該方法將通過getDefaultSize()得到的寬高值作為參數(shù)傳進(jìn)去,看下源碼都干了些什么:

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

該方法會(huì)在開始判斷l(xiāng)ayoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情況,這種特例很少見,我們直接忽略掉。

setMeasuredDimension()方法最后將寬高值傳遞給方法setMeasuredDimensionRaw(),我們?cè)傺芯恳幌?code>setMeasuredDimensionRaw()這方法。

setMeasuredDimensionRaw()

該方法接受兩個(gè)參數(shù),也就是測(cè)量完的寬度和高度,看源碼:

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

看到了吧,這里就是把測(cè)量完的寬高值賦值給mMeasuredWidthmMeasuredHeight這兩個(gè)View的屬性,然后將標(biāo)志位置為已測(cè)量狀態(tài)。

View寬高尺寸值的state

至此,由父ViewGroup發(fā)起的向它內(nèi)部的每個(gè)子View發(fā)送measure命令,然后各個(gè)View根據(jù)ViewGroup給的限制條件測(cè)量出來(lái)的寬高尺寸已經(jīng)存到View的mMeasuredWidthmMeasuredHeight這兩個(gè)屬性當(dāng)中。但ViewGroup怎么知道他的子View是多大呢?View提供了以下三組方法:

  1. getMeasuredWidth()getMeasuredHeight()
  2. getMeasuredWidthAndState()getMeasuredHeightAndState()
  3. getMeasuredState()

通過方法名稱可以猜出寬高的尺寸有state這個(gè)概念,我們?cè)賮?lái)研究View中保存測(cè)量結(jié)果的屬性mMeasuredWidthmMeasuredHeight,其實(shí)只要討論mMeasuredWidth就可以了,mMeasuredHeight一樣的道理。

mMeasuredWidth是一個(gè)Int類型的值,其是由4個(gè)字節(jié)組成的。

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

  • 其高位的第一個(gè)字節(jié)為第一部分,用于標(biāo)記測(cè)量完的尺寸是不是達(dá)到了View想要的寬度,我們稱該信息為測(cè)量的state信息。
  • 其低位的三個(gè)字節(jié)為第二部分,用于存儲(chǔ)測(cè)量到的寬度。

一個(gè)變量能包含兩個(gè)信息,這個(gè)有點(diǎn)類似于measureSpec,但是二者又有不同:

  • measureSpec是將限制條件從ViewGroup傳遞給其子View。
  • mMeasuredWidthmMeasuredHeight是將帶有測(cè)量結(jié)果的state標(biāo)志位信息從View傳遞給其父ViewGroup。

那是在哪里有對(duì)mMeasuredWidth的第一個(gè)字節(jié)進(jìn)行處理呢?可以看到我們下面看一下View中的resolveSizeAndState()方法。

resolveSizeAndState()

這是View一個(gè)很重要的測(cè)量方法,直接看源碼:

    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:
            //當(dāng)specMode為AT_MOST時(shí),這時(shí)候specSize是父ViewGroup給該View指定的最大尺寸
                if (specSize < size) {
                  //如果父ViewGroup指定的最大尺寸比View想要的尺寸還要小,這時(shí)候會(huì)使用MEASURED_STATE_TOO_SMALL這個(gè)掩碼向已經(jīng)測(cè)量出來(lái)的尺寸specSize加入尺寸太小的標(biāo)志,然后將這個(gè)帶有標(biāo)志的specSize返回
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                  //如果父控件指定最大尺寸沒有比View想要的尺寸小,這時(shí)候就放棄之前已經(jīng)給View賦值的specSize,用View自己想要的尺寸就可以了。
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

這個(gè)方法的代碼結(jié)構(gòu)跟前文提到的getDefaultSize()方法很相似,主要的區(qū)別在于specMode為AT_MOST的情況。我們當(dāng)時(shí)說(shuō)getDefaultSize()方法是沒有適配wrap_content這種情況,而這個(gè)resolveSizeAndState()方法是已經(jīng)適配了wrap_content的布局方式,那具體怎么實(shí)現(xiàn)AT_MOST測(cè)量邏輯的呢?有兩種情況:

  • 當(dāng)父ViewGroup指定的最大尺寸比View想要的尺寸還要小時(shí),會(huì)給這個(gè)父ViewGroup的指定的最大值specSize加入一個(gè)尺寸太小的標(biāo)志MEASURED_STATE_TOO_SMALL,然后將這個(gè)帶有標(biāo)志的尺寸返回,父ViewGroup通過該標(biāo)志就可以知道分配給View的空間太小了,在窗口協(xié)商測(cè)量的時(shí)候會(huì)根據(jù)這個(gè)標(biāo)志位來(lái)做窗口大小的決策。
  • 當(dāng)父ViewGroup指定的最大尺寸比沒有比View想要的尺寸小時(shí)(相等或者View想要的尺寸更小),直接取View想要的尺寸,然后返回該尺寸。

getDefaultSize()方法只是onMeasure()方法中獲取最終尺寸的默認(rèn)實(shí)現(xiàn),其返回的信息比resolveSizeAndState()要少,那么什么時(shí)候才會(huì)調(diào)用resolveSizeAndState()方法呢? 主要有兩種情況:

  • Android中的大部分layout類都調(diào)用了resolveSizeAndState()方法,比如LinearLayout在測(cè)量過程中會(huì)調(diào)用resolveSizeAndState()方法而非getDefaultSize()方法。
  • 我們自己在實(shí)現(xiàn)自定義的View或ViewGroup時(shí),我們可以重寫onMeasure()方法,并在該方法內(nèi)調(diào)用resolveSizeAndState()方法。

getMeasureXXX系列方法

現(xiàn)在回過頭來(lái)看下以下三組方法:

  • getMeasuredWidth()getMeasuredHeight()

    該組方法只返回測(cè)量結(jié)果的尺寸信息,去除掉高位字節(jié)的state信息,以getMeasuredWidth()為例,源碼如下:

        public final int getMeasuredWidth() {
          // MEASURED_SIZE_MASK = 0x00ffffff,mMeasuredWidth與MEASURED_SIZE_MASK作與運(yùn)算,
          // 就能將mMeasuredWidth的最高字節(jié)全部置0,從而去掉state信息
            return mMeasuredWidth & MEASURED_SIZE_MASK;
        }
    
  • getMeasuredWidthAndState()getMeasuredHeightAndState()

    該組方法返回測(cè)量結(jié)果同時(shí)包含尺寸和state信息,以getMeasuredWidthAndState()為例,源碼如下:

        public final int getMeasuredWidthAndState() {
          //由于mMeasuredWidth完整包含了尺寸和state信息,直接返回該信息
            return mMeasuredWidth;
        }
    

    ?

  • getMeasuredState()

    該方法返回一個(gè)int值,該值同時(shí)包含寬度的state以及高度的state信息,不包含任何的尺寸信息,源碼如下:

        public final int getMeasuredState() {
          //將寬度state信息保存到int值的第一個(gè)字節(jié)中
          //將高度state信息保存到int值的第三個(gè)字節(jié)中
            return (mMeasuredWidth&MEASURED_STATE_MASK)
                    | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
        }
    
    • MEASURED_STATE_MASK的值為0xff000000,其高字節(jié)的8位全部為1,低字節(jié)的24位全部為0。
    • MEASURED_HEIGHT_STATE_SHIFT值為16。
    • 將MEASURED_STATE_MASK與mMeasuredWidth做與操作之后就取出了存儲(chǔ)在寬度首字節(jié)中的state信息,過濾掉低位三個(gè)字節(jié)的尺寸信息。
    • 由于int有四個(gè)字節(jié),首字節(jié)已經(jīng)存了寬度的state信息,那么高度的state信息就不能存在首位字節(jié)。MEASURED_STATE_MASK向右移16位,變成了0x0000ff00,這個(gè)值與高度值mMeasuredHeight做與操作就取出了mMeasuredHeight第三個(gè)字節(jié)中的信息。而mMeasuredHeight的state信息是存在首字節(jié)中,所以也得對(duì)mMeasuredHeight向右移相同的位置,這樣就把state信息移到了第三個(gè)字節(jié)中。
    • 最后,將得到的寬度state與高度state按位或操作,這樣就拼接成一個(gè)int值,該值首個(gè)字節(jié)存儲(chǔ)寬度的state信息,第三個(gè)字節(jié)存儲(chǔ)高度的state信息。

ViewGroup的measure過程

通過上面的介紹已經(jīng)知道了單個(gè)View的測(cè)量過程,現(xiàn)在看下ViewGroup是怎樣測(cè)量的。

對(duì)于ViewGroup來(lái)說(shuō),除了完成自己的measure過程,還會(huì)遍歷去調(diào)用所有子元素的measure()方法,各個(gè)子元素再遞歸去執(zhí)行這個(gè)過程。和View不同的是,ViewGroup是一個(gè)抽象類,它提供了一個(gè)叫measureChildren()的方法用于測(cè)量子元素,源碼如下:

    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];
            //遍歷每個(gè)子元素,如果該子元素不是GONE的話,就去測(cè)量該子元素
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

從上述代碼來(lái)看,ViewGroup在measure時(shí),會(huì)調(diào)用measureChild()這個(gè)方法對(duì)每一個(gè)子元素進(jìn)行測(cè)量,該方法源碼如下:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //獲取child自身的LayoutParams屬性
        final LayoutParams lp = child.getLayoutParams();
        //根據(jù)父布局的MeasureSpec,父布局的padding和child的LayoutParams這三個(gè)參數(shù),通過getChildMeasureSpec()方法計(jì)算出子元素的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //調(diào)用measure()方法測(cè)量child,前文已經(jīng)解釋過這個(gè)方法,調(diào)用該方法之后會(huì)將view的寬高值保存在mMeasuredWidth和mMeasuredHeight這兩個(gè)屬性當(dāng)中,這樣child的尺寸就已經(jīng)測(cè)量出來(lái)了
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

很顯然,measureChild()的思想就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec()方法來(lái)創(chuàng)建子元素的MeasureSpec,接著將MeasureSpec傳給View的measure()方法來(lái)完成對(duì)子元素的測(cè)量。重點(diǎn)看下getChildMeasureSpec()這個(gè)方法。

getChildMeasureSpec()

該方法是根據(jù)父容器的MeasureSpec、padding和子元素的LayoutParams屬性得到子元素的MeasureSpec,進(jìn)而根據(jù)這個(gè)MeasureSpec來(lái)測(cè)量子元素。源碼如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //取得SpecMode和SpecSize
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //子元素的可用大小為父容器的尺寸減去padding
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        //父容器是EXACTLY模式,表明父容器本身的尺寸已經(jīng)是確定的了
        case MeasureSpec.EXACTLY:
            //childDimension是子元素的屬性值,如果大于等于0,就說(shuō)明該子元素是指定寬/高尺寸的(比如20dp),
            //因?yàn)镸ATCH_PARENT的值為-1,WRAP_CONTENT的值為-2,都是小于0的,所以大于等于0肯定是指定固定尺寸的。
            //既然子元素都指定固定大小了,就直接取指定的尺寸,
            //然后將子元素的測(cè)量模式定為EXACTLY模式,表明子元素的尺寸也確定了
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 如果子元素是MATCH_PARENT,也就是希望占滿父容器的空間,那子元素的尺寸就取父容器的可用空間大小,模式也是EXACTLY,表明子元素的尺寸也確定了
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // 如果子元素是WRAP_CONTENT,也就是寬/高希望能包裹自身的內(nèi)容就可以了,
              //但由于這時(shí)子元素自身還沒測(cè)量,無(wú)法知道自己想要多大的尺寸,
              //所以這時(shí)就先取父容器給子元素留下的最大空間,模式為AT_MOST,表示子元素的寬/高不能超過該最大值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器的尺寸還沒確定,但是不能超過最大值
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // 子元素指定了大小,就取子元素的尺寸,模式為EXACTLY,表明該子元素確定了尺寸
                // 這時(shí)父容器的限制對(duì)子元素來(lái)說(shuō)是不起作用的,子元素的尺寸是可以超出了父容器的大小,超出的部分是顯示不出來(lái)的
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
               // 子元素是MATCH_PARENT,表明子元素希望占滿父容器,
               //但是父容器自身的大小還沒確定,也無(wú)法給子元素確切的尺寸,
               //這時(shí)就先取父容器給子元素留下的最大空間,模式為AT_MOST,表示子元素不能超過該最大值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子元素的尺寸只希望能包裹自身的內(nèi)容就可以了,這時(shí)子元素還沒測(cè)量,無(wú)法知道具體尺寸,
                // 就先取父容器給子元素留下的最大空間,模式為AT_MOST,表示子元素不能超過該最大值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器沒有對(duì)子元素的大小進(jìn)行約束
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // 子元素指定了大小,就取子元素的尺寸,模式為EXACTLY,表明該子元素確定了尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子元素想要占滿父容器,先判斷下子元素是否需要取0,
                // 如果不需要取0,就先取父容器給子元素留下的最大空間,模式為UNSPECIFIED,表示子元素并沒有受到約束
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子元素的尺寸只希望能包裹自身的內(nèi)容就可以了,判斷下需不需要取0,
                // 如果不需要就先取父容器給子元素留下的最大空間,模式為UNSPECIFIED,表示子元素并沒有受到約束
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //根據(jù)得到的大小和模式返回一個(gè)MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

getChildMeasureSpec()這個(gè)方法清楚展示了普通View的MeasureSpec的創(chuàng)建規(guī)則,每個(gè)View的MeasureSpec狀態(tài)量由其直接父View的MeasureSpec和View自身的屬性LayoutParams(LayoutParams有寬高尺寸值等信息)共同決定。總結(jié)為下表:

View的布局屬性\父ViewGroup的MeasurSpec EXACTLY AT_MOST UNSPECIFIED
非負(fù)具體值 EXACTLY childSize EXACTLY childSize EXACTLY childSize
match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0/parentSize
wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0/parentSize

在得到View的MeasureSpec狀態(tài)后,將其與尺寸值通過makeMeasureSpec(int size,int mode)方法結(jié)合在一起,就是最終傳給View的onMeasure(int, int)方法的MeasureSpec值了。

查看源碼發(fā)現(xiàn),ViewGroup并沒有定義其測(cè)量的具體過程方法,這是因?yàn)閂iewGroup是一個(gè)抽象類,其測(cè)量過程的onMeasure()方法需要各個(gè)子類去實(shí)現(xiàn),比如LinearLayout、RelativeLayout、ListView等。為什么ViewGroup不像View一樣對(duì)其onMeasure()方法做統(tǒng)一的實(shí)現(xiàn)呢?那是因?yàn)椴煌腣iewGroup子類有不同的布局特性,這導(dǎo)致它們的測(cè)量細(xì)節(jié)各不相同,因此ViewGroup無(wú)法做統(tǒng)一的實(shí)現(xiàn)。

需要注意的是,雖然View實(shí)現(xiàn)了onMeasure()方法,但也只是一種默認(rèn)實(shí)現(xiàn),前面也提到過View的這種默認(rèn)實(shí)現(xiàn)是不區(qū)分wrap_contentmatch_parent的,而View的子類如果需要支持區(qū)分實(shí)現(xiàn)這兩種布局方式,就需要根據(jù)自身的特性自定義實(shí)現(xiàn)onMeasure()方法,比如TextView、ImageView等就都實(shí)現(xiàn)了onMeasure()方法,而且實(shí)現(xiàn)的方式各不相同,有興趣的同學(xué)可以去看下源碼,這里就不細(xì)講了。

DecorView和ViewRootImpl

本來(lái)關(guān)于View的measure過程到這里已經(jīng)介紹得七七八八了,但是為了更好的理解整個(gè)View樹結(jié)構(gòu)的測(cè)量過程,這里就先簡(jiǎn)單提下DecorViewViewRootImpl這兩個(gè)家伙。

我們知道,Android界面上的View其實(shí)是一個(gè)View樹結(jié)構(gòu),而DecorView就是View樹的頂端,是視圖的頂級(jí)View,一般情況下它內(nèi)部會(huì)包含一個(gè)豎直方向的LinearLayout,在這個(gè)LinearLayout里面有上下兩個(gè)部分(具體情況和Android版本以及主題有關(guān)),上面是標(biāo)題欄,下面是內(nèi)容欄。我們?cè)趧?chuàng)建Activity時(shí)通過setContentView()添加的布局文件其實(shí)就是被加到內(nèi)容欄之中,而內(nèi)容欄是一個(gè)id為content的FrameLayout,所以可以理解Activity指定布局的方法不叫setView()而叫setContentView()了吧。

DecorView.PNG

每一個(gè)Activity組件都有一個(gè)關(guān)聯(lián)的Window對(duì)象,用來(lái)描述一個(gè)應(yīng)用程序窗口。每一個(gè)應(yīng)用程序窗口內(nèi)部又包含有一個(gè)View對(duì)象,用來(lái)描述應(yīng)用程序窗口的視圖。在Activity創(chuàng)建完畢后,DecorView會(huì)被添加到Window中,之后我們才能在屏幕上看到應(yīng)用程序的視圖效果。

而ViewRootImpl是連接WindowManager和DecorView的紐帶,控件的測(cè)量、布局、繪制以及輸入事件的分發(fā)處理都由ViewRootImpl觸發(fā)。它是WindowManagerGlobal工作的實(shí)際實(shí)現(xiàn)者,因此它還需要負(fù)責(zé)與WMS交互通信以調(diào)整窗口的位置大小,以及對(duì)來(lái)自WMS的事件(如窗口尺寸改變等)作出相應(yīng)的處理。它調(diào)用了一個(gè)performTraversals()方法使得View樹開始三大工作流程,然后使得View展現(xiàn)在我們面前。關(guān)鍵源碼如下:

private void performTraversals() {
            ...

        if (!mStopped || mReportNextDraw) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    
            ...
            }
        } 

        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
        }


        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
        }
        ...
}

我們看到它里面執(zhí)行了三個(gè)方法,分別是performMeasure()performLayout()performDraw()這三個(gè)方法,這三個(gè)方法分別完成DecorView的measure、layout、和draw這三大流程,其中performMeasure()中會(huì)調(diào)用measure()方法,在measure()方法中又會(huì)調(diào)用onMeasure()方法,在onMeasure()方法中會(huì)對(duì)所有子元素進(jìn)行measure過程,這個(gè)時(shí)候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會(huì)重復(fù)父容器的measure過程,如此反復(fù)就實(shí)現(xiàn)了從DecorView開始對(duì)整個(gè)View樹的遍歷測(cè)量,measure過程就這樣完成了。同理,performLayout()performDraw()也是類似的傳遞流程。針對(duì)performTraveals()的大致流程,可以用以下流程圖來(lái)表示。

perforTraversals.PNG

performTraversals()方法中,其實(shí)對(duì)于View樹的測(cè)量、布局、繪制不是簡(jiǎn)單地依次單次執(zhí)行,以上的流程圖只是一個(gè)為了便于理解而簡(jiǎn)化版的流程,真正的流程應(yīng)該分為以下五個(gè)工作階段:

  1. 預(yù)測(cè)量階段:這是進(jìn)入performTraversals()方法后的第一個(gè)階段,它會(huì)對(duì)View樹進(jìn)行第一次測(cè)量。在此階段中將會(huì)計(jì)算出View樹為顯示其內(nèi)容所需的尺寸,即期望的窗口尺寸。(調(diào)用measureHierarchy()
  2. 布局窗口階段:根據(jù)預(yù)測(cè)量的結(jié)果,通過IWindowSession.relayout()方法向WMS請(qǐng)求調(diào)整窗口的尺寸等屬性,這將引發(fā)WMS對(duì)窗口進(jìn)行重新布局,并將布局結(jié)果返回給ViewRootImpl。(調(diào)用relayoutWindow()
  3. 最終測(cè)量階段:預(yù)測(cè)量的結(jié)果是View樹所期望的窗口尺寸。然而由于在WMS中影響窗口布局的因素很多,WMS不一定會(huì)將窗口準(zhǔn)確地布局為View樹所要求的尺寸,而迫于WMS作為系統(tǒng)服務(wù)的強(qiáng)勢(shì)地位,View樹不得不接受WMS的布局結(jié)果。因此在這一階段,performTraversals()將以窗口的實(shí)際尺寸對(duì)View樹進(jìn)行最終測(cè)量。(調(diào)用performMeasure()
  4. 布局View樹階段:完成最終測(cè)量之后便可以對(duì)View樹進(jìn)行布局了。(調(diào)用performLayout()
  5. 繪制階段:這是performTraversals()的最終階段。確定了控件的位置與尺寸后,便可以對(duì)View樹進(jìn)行繪制了。(調(diào)用performDraw()

也就是說(shuō),實(shí)際上多了預(yù)測(cè)量階段和布局窗口階段,這里面還有很多可以講的,但本文主要是介紹View的measure過程,相關(guān)性不大的盡量少涉及,以免太過混亂。

預(yù)測(cè)量階段和最終測(cè)量階段都會(huì)至少完整測(cè)量一次View樹,這兩個(gè)階段的區(qū)別也只是參數(shù)不同而已。預(yù)測(cè)量階段用到了一個(gè)measureHierarchy()方法,該方法傳入的參數(shù)desiredWindowWidth與desiredWindowHeight是期望的窗口尺寸。View樹本可以按照這兩個(gè)參數(shù)完成測(cè)量,但是measureHierarchy()有自己的考量,即如何將窗口布局地盡可能地優(yōu)雅。

這是針對(duì)將LayoutParams.width設(shè)置為了WRAP_CONTENT的懸浮窗口而言。如前文所述,在設(shè)置為WRAP_CONTENT時(shí),指定的desiredWindowWidth是應(yīng)用可用的最大寬度,如此可能會(huì)產(chǎn)生下面左圖所示的丑陋布局。這種情況較容易發(fā)生在AlertDialog中,當(dāng)AlertDialog需要顯示一條比較長(zhǎng)的消息時(shí),由于給予的寬度足夠大,因此它有可能將這條消息以一行顯示,并使得其窗口充滿了整個(gè)屏幕寬度,在橫屏模式下這種布局尤為丑陋。

倘若能夠?qū)捎脤挾冗M(jìn)行適當(dāng)?shù)南拗疲仁笰lertDialog將消息換行顯示,則產(chǎn)生的布局結(jié)果將會(huì)優(yōu)雅得多,如圖下面右圖所示。但是,倘若不分清紅皂白地對(duì)寬度進(jìn)行限制,當(dāng)控件樹真正需要足夠的橫向空間時(shí),會(huì)導(dǎo)致內(nèi)容無(wú)法顯示完全,或者無(wú)法達(dá)到最佳的顯示效果。例如當(dāng)一個(gè)懸浮窗口希望盡可能大地顯示一張照片時(shí)就會(huì)出現(xiàn)這樣的情況。

alertdialog.PNG

那么measureHierarchy()如何解決這個(gè)問呢?它采取了與View樹進(jìn)行協(xié)商的辦法,即先使用measureHierarchy()所期望的寬度限制嘗試對(duì)View樹進(jìn)行測(cè)量,然后通過測(cè)量結(jié)果來(lái)檢查View樹是否能夠在此限制下滿足其充分顯示內(nèi)容的要求。倘若沒能滿足,則measureHierarchy()進(jìn)行讓步,放寬對(duì)寬度的限制,然后再次進(jìn)行測(cè)量,再做檢查。倘若仍不能滿足則再度進(jìn)行讓步。

關(guān)鍵源碼如下:

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        // 表示測(cè)量結(jié)果是否可能導(dǎo)致窗口的尺寸發(fā)生變化
        boolean windowSizeMayChange = false;

        //goodMeasure表示了測(cè)量是否能滿足View樹充分顯示內(nèi)容的要求
        boolean goodMeasure = false;
        //測(cè)量協(xié)商僅發(fā)生在LayoutParams.width被指定為WRAP_CONTENT的情況下
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //第一次協(xié)商。measureHierarchy()使用它最期望的寬度限制進(jìn)行測(cè)量。
            //這一寬度限制定義為一個(gè)系統(tǒng)資源。
            //可以在frameworks/base/core/res/res/values/config.xml找到它的定義
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            // 寬度限制被存放在baseSize中
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //第一次測(cè)量。調(diào)用performMeasure()進(jìn)行測(cè)量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                
                //View樹的測(cè)量結(jié)果可以通過mView的getmeasuredWidthAndState()方法獲取。
                //View樹對(duì)這個(gè)測(cè)量結(jié)果不滿意,則會(huì)在返回值中添加MEASURED_STATE_TOO_SMALL位
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;  // 控件樹對(duì)測(cè)量結(jié)果滿意,測(cè)量完成
                } else {
                  //第二次協(xié)商。上次的測(cè)量結(jié)果表明View樹認(rèn)為measureHierarchy()給予的寬度太小,在此
                  //在此適當(dāng)?shù)胤艑拰?duì)寬度的限制,使用最大寬度與期望寬度的中間值作為寬度限制
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                  //第二次測(cè)量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                  // 再次檢查控件樹是否滿足此次測(cè)量
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                     // 控件樹對(duì)測(cè)量結(jié)果滿意,測(cè)量完成
                        goodMeasure = true;
                    }
                }
            }
        }
        
        if (!goodMeasure) {
          //最終測(cè)量。當(dāng)View樹對(duì)上述兩次協(xié)商的結(jié)果都不滿意時(shí),measureHierarchy()放棄所有限制
          //做最終測(cè)量。這一次將不再檢查控件樹是否滿意了,因?yàn)榧幢闫洳粷M意,measurehierarchy()也沒
          //有更多的空間供其使用了
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
          //如果測(cè)量結(jié)果與ViewRootImpl中當(dāng)前的窗口尺寸不一致,則表明隨后可能有必要進(jìn)行窗口尺寸的調(diào)整
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }

        // 返回窗口尺寸是否可能需要發(fā)生變化
        return windowSizeMayChange;
    }

可以看到,measureHierarchy()方法最終也是調(diào)用了performMeasure()方法對(duì)View樹進(jìn)行測(cè)量,只是多了協(xié)商測(cè)量的過程。

顯然,對(duì)于非懸浮窗口,即當(dāng)LayoutParams.width被設(shè)置為MATCH_PARENT時(shí),不存在協(xié)商過程,直接使用給定的desiredWindowWidth/Height進(jìn)行測(cè)量即可。而對(duì)于懸浮窗口,measureHierarchy()可以連續(xù)進(jìn)行兩次讓步,從而導(dǎo)致View的onMeasure()方法多次被調(diào)用。

這里也看到,在View的measure過程中設(shè)置的MEASURED_STATE_TOO_SMALL標(biāo)志位就在測(cè)量協(xié)商過程中起作用了。

window.PNG

總結(jié)

看到了這里,我們發(fā)現(xiàn)Android中View的measure過程是很巧妙的,知道如何利用以前測(cè)量過的數(shù)據(jù),如果情況有變,那么就調(diào)用onMeasure()方法進(jìn)行實(shí)際的測(cè)量工作。真正實(shí)現(xiàn)對(duì)View本身的測(cè)量就是在onMeasure()中,在該方法中View要根據(jù)父ViewGroup給其傳遞進(jìn)來(lái)的widthMeasureSpec和heightMeasureSpec,并結(jié)合View自身想要的尺寸,綜合考慮,計(jì)算出最終的寬度和高度,并存儲(chǔ)到相應(yīng)的成員變量中,這才標(biāo)志著該View測(cè)量有效的完成了,如果沒有將值存入到成員變量中,View會(huì)拋出異常。而且在成員變量中還儲(chǔ)藏著測(cè)量的狀態(tài)信息state,該信息表示了View對(duì)此次測(cè)量的結(jié)果是否滿意。而這個(gè)state信息有可能會(huì)在ViewRootImpl在做窗口大小決策的時(shí)候提供反饋,從而達(dá)到最佳的顯示效果。

相關(guān)閱讀:

http://blog.csdn.net/iispring/article/details/49403315
http://blog.csdn.net/a553181867/article/details/51494058

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容