1. 程式人生 > >View的繪制流程

View的繪制流程

藝術探索 約束 不同 中大 adding getchild ack else if ams

這是年假最後一篇筆記了,本篇文章的內容主要來自《android開發藝術探索》,在文章的最後有這本書的網上版本。

項目源碼

目錄

  • MeasureSpec
    • SpecMode分類
      • UNSPECIFIED
      • EXACTLY
      • AT_MOST
    • MeasureSpec和LayoutParams對應關系
  • measure過程
    • View的measure過程

1. MeasureSpec

MeasureSpec代表的是一個32位的int類型的數值,31 ~ 30為測量模式(SpecMode),29 ~ 0(SpecSize) 為寬高的實際大小。一個完整的MeasureSpec是由SpecMode+SpecSize組合而成,可通過makeMeasureSpec()

得到MeasureSpec、通過getMode()得到SpecMode、通過getSize()得到SpecSize。

//打包生成MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
//解包得到SpecMode
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
//解包得到SpecSize
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

SpecMode分類

模式 二進制數位 描述
UNSPECIFIED 00 父容器不對View有任何限制,要多大給多大,一般用於系統內部表示一種測量狀態
EXACTLY 01 表示父控件已經測量出View的大小。View的最終大小就是SpecSize指定的大小;它對應兩種模式第一種對應LayoutParams的match_parent,另一種是具體的數值
AT_MOST 10 父容器指定一個SpecSize,View的大小不能不能超過這個值。對應的是LayoutParams的wrap_content

MeasureSpec和LayoutParams對應關系

LayoutParams配合父容器的MeasureSpec用於約束View的大小,他們兩個共同作用下 生成最終的View的MeasureSpec,從而確定View的寬高。需要註意的是頂層View和普通View的測量有所不同。DecorView的MeasureSpec是由窗口的尺寸

和自身LayoutParams共同作用生成,普通View是由父容器的MeasureSpec和自身LayoutParams共同作用生成。

頂層view的MeasureSpec生成過程:

……
//獲取頂層View的寬高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
……
//生成頂層View的MeasureSpec
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // 精度模式,頂層View的大小就是窗口的大小
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // 最大模式,大小不確定,但頂層View的大小不能超過窗口的大小
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // 精度模式,頂層View的大小為LayoutParams的大小
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

對於普通View的measure,是由ViewGroup的measure發起的。ViewGroup會調用他的measureChild()來測量子View的寬高在該方法內部會調用getChildMeasureSpec()獲取View的MeasureSpec。
下面為measureChild()代碼:

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    //獲取子View寬度MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    //獲取子View高度MeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

下面為getChildMeasureSpec()代碼:

//子View的大小會受父容器的MeasureSpec、自身的LayoutParams、View的padding以及margin影響。
public static int getChildMeasureSpec(int spec, int padding, int childDimension){
        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) {
        // 父容器為具體精度
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //子控件的寬或高大於0,代表其設置了具體的寬高值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子元素為精度模式,占滿父容器的剩余空間
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //當子控件為WRAP_CONTENT的時候不管父控件是精度模式還是最大
                //化模式,View的模式總是最大化,並且不會超過父容器的剩余空間
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // 父容器為最大化模式
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // 子控件設置了具體的值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子view為精度模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子元素為最大化模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // 父元素為不受限制模式
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

從上面的代碼可以知道,返回View的MeasureSpec大致可以分為一下機制情況:

  • 子View為具體的寬/高,那麽View的MeasureSpec都為LayoutParams中大小。
  • 子View為match_parent,父元素為精度模式(EXACTLY),那麽View的MeasureSpec也是精準模式他的大小不會超過父容器的剩余空間。
  • 子View為wrap_content,不管父元素是精準模式還是最大化模式(AT_MOST),View的MeasureSpec總是為最大化模式並且大小不超過父容器的剩余空間。
  • 父容器為UNSPECIFIED模式主要用於系統多次Measure的情形,一般我們不需要關心。

技術分享圖片

2. measure過程

View的measure過程

view測量的過程是由measure()方法完成。該方法不能被重寫(是final類型方法),在該方法內部調用了onMeasure()方法用於測量View的大小:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //設置view的寬/高
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

從上面的代碼中我們可以知道getDefaultSize()為獲取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()放回的大小兩種,第一種為當specMode為AT_MOST、EXACTLY情況下View的大小為specSize也就是測量後的大小,View的最終大小是在layout階段確認下來的,不過view的測量大小和最終大小,幾乎所有情況下都是相等的。
第二種情況為specMode為UNSPECIFIED,這種模式一般用於系統內部的測量過程,該模式下View的大小為傳入getDefaultSize()方法的第一個參數size,從上面的代碼可以知道,Size為getSuggestedMinimumWidth()或getSuggestedMinimumHeight()返回的大小。

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}

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

getSuggestedMinimumWidth()和getSuggestedMinimumHeight一樣,我們只需要分析一個即可,下面以getSuggestedMinimumWidth()為例:
返回的大小與有沒有設置背景有關,當View沒有設置背景,返回的為mMinHeight。該值為android.minWidth指定的值(默認為0),如果View指定了背景,view返回的值為max(mMinWidth, mBackground.getMinimumWidth())。

public int getMinimumWidth() {
    //獲取Drawable的原始高度,如果沒有原始高度返回的為-1。如:ShapeDrawable無原始高度,BitmapDrawable有原始高度。
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

總結:

一般我們在自定View的時候需要重寫onMeasure()方法,因為從上面的圖表中我們可以知道,當我們指定的屬性為warp_content的時候系統返回的是父容器剩余空間的大小,這樣就和指定的match_parent給的大小一致了。下面為解決這個問題的方式:

private int mWidth = 200;
private int mHeight = 200;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(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);
    }
}

參考

Android開發藝術探索完結篇——天道酬勤

自定義View,有這一篇就夠了

View的繪制流程