天天看點

Android開發——View繪制過程源碼解析(一)

0. 前言  

View的繪制流程從ViewRoot的performTraversals開始,經過measure,layout,draw三個流程,之後就可以在螢幕上看到View了。其中measure用于測量View的寬和高,layout用于确定View在父容器中放置的位置,draw則用于将View繪制到螢幕上。

本文原創,轉載請注明出處:SEU_Calvin的CSDN部落格。

1. MeasureSpec

說到measure那麼就不得不提MeasureSpec,一旦确定了MeasureSpec,在onMeasure()中就可以确定View的寬高。

MeasureSpec的值由SpecSize(測量值)和SpecMode(測量模式)共同組成。它是由布局參數和父容器的測量屬性一起決定的。

其中測量模式一共有三種類型:

(1)EXACTLY:表示精确模式,一般當childView設定其寬高為精确值、match_parent(同時父容器也是這種模式)的情況。

(2)AT_MOST:表示最大值模式,一般當childView設定其寬高為wrap_content、match_parent(同時父容器也是這種模式)的情況。

(3)UNSPECIFIED:表示子視圖可以想要任何尺寸,一般用于系統内部,開發時很少使用。

2. MeasureSpec的生成過程

2.1 頂級View的MeasureSpec

// desiredWindowWidth和desiredWindowHeight為螢幕尺寸
// lp.width和lp.height都等于MATCH_PARENT
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 
//…
private int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  
    case ViewGroup.LayoutParams.MATCH_PARENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
}  
           

從源碼中可以看出,這裡使用了MeasureSpec.makeMeasureSpec()方法來組裝一個MeasureSpec,rootDimension參數等于MATCH_PARENT,MeasureSpec的SpecMode為EXACTLY。并且MATCH_PARENT和WRAP_CONTENT時的SpecSize都等于windowSize的,也就意味着根視圖總是會充滿全屏的。

總結一下就是,頂級View的測量屬性中,測量大小就是螢幕大小,測量模式就是EXACTLY。

2.2 普通View的MeasureSpec

在對子元素進行measure之前,會先調用getChildMeasureSpec方法得到子元素的測量屬性。上面也提到過了,子元素MeasureSpec與父容器的MeasureSpec和子元素本身的LayoutParams有關。

//參1為父容器的MeasureSpec
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);
}
           

getChildMeasureSpec()方法的源碼裡的邏輯:

(1)當View為固定寬高時,測量模式是EXACTLY模式,測量值就是布局參數中的大小。

(2)當View為WRAP_CONTENT時,測量模式是AT_MOST模式,測量值是父容器的剩餘空間大小。

(3)當View為MATCH_PARENT時,測量值是父容器的剩餘空間大小,測量模式分兩種情況,如果父容器是EXACTLY模式,那就是EXACTLY模式,如果父容器是AT_MOST模式,那麼View也是AT_MOST模式。

3. Measure過程

3.1 普通View的Measure過程

View的measure()方法是final的,是以我們無法在子類中去重寫這個方法,在該方法内部會調用onMeasure()方法,源碼如下所示。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // setMeasuredDimension設定視圖的大小,這樣就完成了一次measure的過程
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    
}
//這個方法就是近似的傳回spec中的specSize,除非你的specMode是UNSPECIFIED
//UNSPECIFIED 這個一般都是系統内部測量才用的到
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方法完成,這個方法以測量屬性作為參數,并将結果作為參數傳入setMeasuredDimension()方法,完成一次Measure。

在getDefaultSize方法中,不管是測量屬性中的模式是AT_MOST還是EXACTLY,都是直接傳回測量屬性中的測量值。

(1)如果View設定了固定寬高,傳回的就是設定的大小,xdp/ydp。

(2)如果設定了MATCH_PARENT,View的測量模式會有兩種情況,不過不管是哪一種,傳回的都是父容器剩餘大小。

(3)但是如果設定了WRAP_CONTENT,也是父容器剩餘大小,和包裹内容的效果會失效。

這裡就不貼執行個體了,網上有很多,有興趣可以檢視這一篇。

說了這麼多,我們得出的結論就是:繼承View的自定義控件,需要重寫onMeasure()的同時,要設定WRAP_CONTENT時自身大小。

解決方式就是在onMeasure裡針對WRAP_CONTENT屬性直接通過setMeasuredDimension()方法為其指定一個預設的寬高。邏輯如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //預設值
    int desiredWidth = 100;
    int desiredHeight = 100;

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = Math.min(desiredWidth, widthSize);
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY) {
        //Must be this size
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        height = Math.min(desiredHeight, heightSize);
    } else {
        //Be whatever you want
        height = desiredHeight;
    }

    //MUST CALL THIS
    setMeasuredDimension(width, height);
}
           

【思考】我翻了所有的資料,解決這個問題時都預設AT_MOST就等于WRAP_CONTENT。我們知道頂級容器預設是EXACTLY模式,是以在這篇部落格裡的例子中上述代碼可以解決WRAP_CONTENT失效的問題。

但是如果布局參數寫為MATCH_PARENT并且父容器為AT_MOST模式時,得出的子View也是AT_MOST模式,那麼上述代碼好像是有邏輯漏洞的。想了想,好像确實很難出現這種情況,具體不太清楚,有清楚的朋友可以留言交流一下。

3.2 ViewGroup的Measure過程

ViewGroup是沒有onMeasure()方法的,因為不同的ViewGroup子類布局都不一樣,是以這個方法是交給子類自己實作的。

ViewGroup中定義了一個measureChildren()方法,其中通過for循環來周遊測量子視圖的大小,如下所示:

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

周遊的過程中調用measureChild()方法,傳入子View參數和父容器的測量屬性,先得出子View的測量屬性,在把它作為參數傳給子View的measure()方法,完成一個子View的測量。其實作為:

protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {    
       //通過子布局參數和父容器MeasureSpec得到childMeasureSpec
       //過程略..
       //是以這其實是個遞歸調用,不斷的去測量設定子視圖的大小,直至完成整個View數測量周遊 
       child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    
}
           

以上就是關于measure過程的一些解析,後面會更新另外layout和draw過程的解析。

本文原創,轉載請注明出處:SEU_Calvin的CSDN部落格。歡迎留言交流,謝謝。

Android開發——View繪制過程源碼解析(一)