onMeasure流程解析

0、預備知識android

咱們的手機屏幕的佈局實際上是嵌套的,最外層是一個phoneWindow,這個view和手機屏幕同樣大,裏面是一個frameLayout,再裏面纔是咱們本身寫的佈局文件。canvas

咱們在繪製控件前必需要經歷measure的過程,這個過程須要從最外層的PhoneWindow開始進行。phonewindow調用內部frameLayout的measure,frameLayout又調用內部view的onMeasure,依次類推。總之就是不斷的調用本身內部view的measure方法(measure中會調用onMeasure),直到到達了最內部的view。其實說白了就是這麼個流程,下面就是onMeasure方法:緩存

void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

這個方法真是簡單直觀,讓人一會兒以爲都沒什麼可說的了(真的麼?)框架

沒有返回值,只有兩個參數,咱們惟一須要分析的就是這兩個參數了。然而,經過上面的分析,咱們已經知道了這兩個參數確定是從它外層的view傳來的,畢竟外層的view調用了它的measure嘛,而onMeasure又會調用measure,額,貌似又沒啥可說的了。姑且說下這兩個參數是怎麼傳進來的,參數的意義又是什麼吧。ide

 

1.childWidthMeasureSpec和childHeightMeasureSpec佈局

首先最外層的view調用了performTraversals方法,獲得了childWidthMeasureSpec和childHeightMeasureSpec兩個值:優化

private void performTraversals() {  
    // ………省略宇宙塵埃數量那麼多的代碼………  
  
    if (!mStopped) {  
        // ……省略一些代碼  
  
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  
  
        // ……省省省  
  
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  
    }  
  
    // ………省略人體細胞數量那麼多的代碼………  
}  

咱們看看上面代碼中的三行代碼是什麼意思:this

第一行、第二行的操做同樣,咱們分析第一行就得了。獲得widthMeasureSpec的途徑是調用getRootMeasureSpec作的,給他傳的是width和lp.width。這兩個值咱們太熟悉了,每一個view裏面都有。spa

1.view的width:這個不用多說,須要說明的是最外層的view(phoneWindow)寬度確定和手機屏幕同樣,而手機屏幕的寬度是肯定的,因而這個值在最外層就有了初始值。.net

2.lp.width:

TextView view = (TextView) findViewById(R.id.text);
ViewGroup.LayoutParams lp = view.getLayoutParams();
lp.width; lp.height;

這個東西咱們也常見,佈局屬性嘛!須要說明的是最外層view的lp.width和lp.height均爲MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams類型)將值賦予給lp時就已被肯定。

 

如今咱們知道參數了意義了,並且這些參數都是咱們熟悉的,但我好好奇,這個getRootMeasure中到底作了什麼不得了的事情!

 //                          參數解釋:        width/height   lp.width/lp.height
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window不能調整其大小,強制使根視圖大小與Window一致  
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window能夠調整其大小,爲根視圖設置一個最大值  
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window想要一個肯定的尺寸,強制將根視圖的尺寸做爲其尺寸  
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
    }

咱們終於遇到不清楚的常量了,不過不用怕!常量嘛,其實就是標識的做用,來看看這些常量是什麼意思:

1. EXACTLY

表示父視圖「但願」子視圖的大小應該是由specSize的值來決定的!

系統默認會按照這個規則來設置子視圖的大小,開發人員固然也能夠按照本身的意願設置成任意的大小。

2. AT_MOST

表示子視圖「最多」只能是specSize中指定的大小!

開發人員應該儘量小得去設置這個視圖,而且保證不會超過specSize。系統默認會按照這個規則來設置子視圖的大小,開發人員固然也能夠按照本身的意願設置成任意的大小。

3. UNSPECIFIED

表示開發人員能夠將視圖按照本身的意願設置成任意的大小,沒有任何限制

這種狀況比較少見,不太會用到。

如今分析以下:

外層view是match_parent時,咱們容許內層view在外層view的所在區域內繪製本身。

當外層view是wrap_content時,咱們應在最外層view的所在區域內儘量小的繪製內層view。

若是外層view有固定大小,那麼咱們內層view的繪製區域就是lp.xxx,由於這時候lp.xxx = 肯定值,因此在這種狀況下內層view的繪製區域就是外部view的繪製區域。

最終咱們經過MeasureSpec.makeMeasureSpc來把這兩個值拼接起來,變成一個64位的值,傳遞下去。

我隱隱的感到,之後咱們仍是要把這個拼接好的值解析出來的,感受MeasureSpec之後還會見到。

 

2.探尋onMeasure方法

view的measure以下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 省略部分代碼……  
  
    /* 
     * 判斷當前mPrivateFlags是否帶有PFLAG_FORCE_LAYOUT強制佈局標記 
     * 判斷當前widthMeasureSpec和heightMeasureSpec是否發生了改變 
     */  
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||  
            widthMeasureSpec != mOldWidthMeasureSpec ||  
            heightMeasureSpec != mOldHeightMeasureSpec) {  
  
        // 若是發生了改變表示須要從新進行測量此時清除掉mPrivateFlags中已測量的標識位PFLAG_MEASURED_DIMENSION_SET  
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;  
  
        resolveRtlPropertiesIfNeeded();  
  
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key);  
        if (cacheIndex < 0 || sIgnoreMeasureCache) {  
            // 測量View的尺寸  
 onMeasure(widthMeasureSpec, heightMeasureSpec);  
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;  
        } else {  
            long value = mMeasureCache.valueAt(cacheIndex);  
  
            setMeasuredDimension((int) (value >> 32), (int) value);  
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;  
        }  
  
        /* 
         * 若是mPrivateFlags裏沒有表示已測量的標識位PFLAG_MEASURED_DIMENSION_SET則會拋出異常 
         */  
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {  
            throw new IllegalStateException("onMeasure() did not set the"  
                    + " measured dimension by calling"  
                    + " setMeasuredDimension()");  
        }  
  
        // 若是已測量View那麼就能夠往mPrivateFlags添加標識位PFLAG_LAYOUT_REQUIRED表示能夠進行佈局了  
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;  
    }  
  
    // 最後存儲測量完成的測量規格  
    mOldWidthMeasureSpec = widthMeasureSpec;  
    mOldHeightMeasureSpec = heightMeasureSpec;  
  
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |  
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension  
}  

過程以下:

判斷傳入的參數是否和以前的同樣,若是同樣,那麼就不作改變(這點是優化中須要注意的,在setVisible中也有相似的判斷)。

若是是強制測量,那麼無論如今的參數和以前的一不同,都進行從新測量。

若是有緩存則用緩存(這裏的緩存也如我所料,用了LongSparseLongArray這種散列表),沒有緩存就調用onMeasure。

測量完畢後設置標誌位而且存儲測量後的數據,最後把這些結果放入緩存。

多說一點:咱們發現用緩存後調用了一個方法:setMeasureDimension。咱們有理由推斷,在onMeasure中確定也須要調用這個方法來設置最終的view大小。

 

咱們知道能夠重寫onMeasure來作本身的測量工做,在此以前咱們先來看看默認的實現方案,以後咱們就能夠照貓畫虎地作啦。

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

這裏面其實就一個setMeasureDimension,驗證了以前的猜測。這個方法傳入了width和height,來最終肯定控件的大小。getDefaultSize方法給人的感受是經過傳入的值和建議的值的獲得一個最終合理的值,我感受裏面有可能用到了各類比較、取大小、取上下限等操做

/**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

好嘛,MeasureSpec你又出現了!如今咱們把以前傳過來的值分前32位後32位獲得了mode和size,若是外層view沒有設置任何測量標準,那麼就用推薦的size,不然就是用外層view傳來的size。

注意:上述代碼中當模式爲AT_MOSTEXACTLY時均會返回計算出的測量尺寸,還記得上面咱們說的PhoneWindow、DecorView麼從它們那裏獲取到的測量規格層層傳遞到咱們的自定義View中,這就是爲何咱們的自定義View在默認狀況下不論是math_parent仍是warp_content都能佔滿父容器的剩餘空間的緣由。

獲得系統推薦的size也很簡單:

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

若是背景爲空那麼咱們直接返回mMinWidth最小寬度不然就在mMinWidth和背景最小寬度之間取一個最大值,反正就是獲得控件的最小大小。哈哈,這裏又印證了個人猜測,用到了比較和取大小來獲得最終的結果。

 

3.自定義onMeasure

咱們已經分析清楚了這樣一個流程,那麼咱們就來自定義一個onMeasure方法吧~

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

哈哈,太簡單了,徹底沒有任何難度嘛。咦,以前說道的UNSPECIFIED、EXACTLY、AT_MOST這些常量好像沒啥用了,size也沒有用到。是否是簡單的有點過度,不太好吧。咱們以前解釋了這些常量的意思(忘記了請回到上面看看,順便看看紅字),他們其實也就是個建議,至於開發者想要怎麼設置view的大小,android框架是無論的。雖然如此,咱們仍是應該作個聽話的孩子,讓view的最終測量尺寸由view自己和外層view共同決定纔好。下面的代碼展現了咱們應如何更好的自定義onMeasure。

 

下面給個源碼:

package com.example.kale.text;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

/**
 * @author Jack Tony
 * @date 2015/8/3
 */
public class TestView extends View {

    Bitmap mBitmap;

    public TestView(Context context) {
        this(context, null);
    }

    public TestView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }


    public void initView() {
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kale);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 聲明一個臨時變量來存儲計算出的測量值  
        int resultWidth = 0;

        // 獲取寬度測量規格中的mode  
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);

        // 獲取寬度測量規格中的size  
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);  
  
    /* 
     * 若是外層view內心有數 
     */
        if (modeWidth == MeasureSpec.EXACTLY) {
            // 取外層view給的大小  
            resultWidth = sizeWidth;
        }  
    /* 
     * 若是外層view沒數 
     */
        else {
            // 可要本身看看本身須要多大了  
            resultWidth = mBitmap.getWidth();  
  
        /* 
         * 若是外層view給的是一個限制值 
         */
            if (modeWidth == MeasureSpec.AT_MOST) {
                // 那麼本身的需求就要跟限制比比看誰小要誰  
                resultWidth = Math.min(resultWidth, sizeWidth);
            }
        }
        resultWidth += getPaddingLeft() + getPaddingRight();

        int resultHeight = 0;
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeHeight == MeasureSpec.EXACTLY) {
            resultHeight = sizeHeight;
        } else {
            resultHeight = mBitmap.getHeight();
            if (modeHeight == MeasureSpec.AT_MOST) {
                resultHeight = Math.min(resultHeight, sizeHeight);
            }
        }
        resultHeight += getPaddingTop() + getPaddingBottom();

        // 設置測量尺寸  
        setMeasuredDimension(resultWidth, resultHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingRight(), null);
    }
}

 

4.viewGroup的measureChild

measureChildWithMargins和measureChildren相似只是加入了對Margins外邊距的處理,ViewGroup提供對子元素測量的方法從measureChildren開始:

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

這裏的操做是遍歷viewgroup中的view,而後在view可見的前提下調用measureChild方法。這裏傳入三個值,參數都是你們熟悉的,就不錯過多說明了。

protected void measureChild(View child, int parentWidthMeasureSpec,  int parentHeightMeasureSpec) {  
    // 獲取子元素的佈局參數  
    final LayoutParams lp = child.getLayoutParams(); 
    /* 
     * 將父容器的測量規格已經上下和左右的邊距還有子元素自己的佈局參數傳入getChildMeasureSpec方法計算最終測量規格 
     */  
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);  
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);  
  
    // 調用子元素的measure傳入計算好的測量規格  
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
} 

getChildMeasureSpec這個方法和getRootMeasureSpec很類似,那麼咱們主要就是看看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) {  
    case MeasureSpec.EXACTLY: // 父容器尺寸大小是一個肯定的值  
        /* 
         * 根據子元素的佈局參數判斷 
         */  
        if (childDimension >= 0) { //若是childDimension是一個具體的值  
            // 那麼就將該值做爲結果  
            resultSize = childDimension;  
  
            // 而這個值也是被肯定的  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) { //若是子元素的佈局參數爲MATCH_PARENT  
            // 那麼就將父容器的大小做爲結果  
            resultSize = size;  
  
            // 由於父容器的大小是被肯定的因此子元素大小也是能夠被肯定的  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) { //若是子元素的佈局參數爲WRAP_CONTENT  
            // 那麼就將父容器的大小做爲結果  
            resultSize = size;  
  
            // 可是子元素的大小包裹了其內容後不能超過父容器  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    case MeasureSpec.AT_MOST: // 父容器尺寸大小擁有一個限制值  
        /* 
         * 根據子元素的佈局參數判斷 
         */  
        if (childDimension >= 0) { //若是childDimension是一個具體的值  
            // 那麼就將該值做爲結果  
            resultSize = childDimension;  
  
            // 而這個值也是被肯定的  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) { //若是子元素的佈局參數爲MATCH_PARENT  
            // 那麼就將父容器的大小做爲結果  
            resultSize = size;  
  
            // 由於父容器的大小是受到限制值的限制因此子元素的大小也應該受到父容器的限制  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) { //若是子元素的佈局參數爲WRAP_CONTENT  
            // 那麼就將父容器的大小做爲結果  
            resultSize = size;  
  
            // 可是子元素的大小包裹了其內容後不能超過父容器  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制  
        /* 
         * 根據子元素的佈局參數判斷 
         */  
        if (childDimension >= 0) { //若是childDimension是一個具體的值  
            // 那麼就將該值做爲結果  
            resultSize = childDimension;  
  
            // 而這個值也是被肯定的  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) { //若是子元素的佈局參數爲MATCH_PARENT  
            // 由於父容器的大小不受限制而對子元素來講也能夠是任意大小因此不指定也不限制子元素的大小  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) { //若是子元素的佈局參數爲WRAP_CONTENT  
            // 由於父容器的大小不受限制而對子元素來講也能夠是任意大小因此不指定也不限制子元素的大小  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
  
    // 返回封裝後的測量規格  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}  

咱們經過上面的代碼知曉了內部view的最終測量值是由它自身和外部的viewGroup共同決定的,最終把這個算好的值傳入到了內部view的measure中,measure由會把這些值傳到onMeasure中。須要再次說明的是,咱們雖然在onMeasure中接收到了這些值,但它們僅僅是一個建議,咱們是仍舊仍是能夠隨意指定的~

 

 

參考自:

http://blog.csdn.net/aigestudio/article/details/42989325

http://blog.csdn.net/guolin_blog/article/details/16330267

相關文章
相關標籤/搜索