View的繪制流程
這是年假最後一篇筆記了,本篇文章的內容主要來自《android開發藝術探索》,在文章的最後有這本書的網上版本。
項目源碼
目錄
- MeasureSpec
- SpecMode分類
- UNSPECIFIED
- EXACTLY
- AT_MOST
- MeasureSpec和LayoutParams對應關系
- SpecMode分類
- measure過程
- View的measure過程
1. MeasureSpec
MeasureSpec代表的是一個32位的int類型的數值,31 ~ 30為測量模式(SpecMode),29 ~ 0(SpecSize) 為寬高的實際大小。一個完整的MeasureSpec是由SpecMode+SpecSize組合而成,可通過makeMeasureSpec()
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是由窗口的尺寸
父容器的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的繪制流程