自定義View的三大流程淺析
目錄
1.自定義View簡介
理解View的基本流程(包括測量流程,佈局流程,繪製流程),可以實現各種效果的自定義View。
自定義View可以分成兩大類:
1.繼承檢視ViewGroup (ViewGroup、LinearLayout、FrameLayout、RelativeLayout等)
2.繼承控制元件View(View、TextView、ImageView、Button等)
View的繪製基本上由measure()、layout()、draw()這個三個函式完成,再講3大流程前,先理解幾個名詞。
2.MeasureSpec
MeasureSpec參與View的測量工作,自身為32位的int值,高二位代表SpecMode(測量模式),低30位代表SpecSize(在某種測量模式下的規格大小)。SpecMode和SpecSize都為int型別。一組SpecMode和SpecSize可以組合成一個MeasureSpec。也可通過MeasureSpec去拿到SpecMode或SpecSize。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ... public static class MeasureSpec { public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; @MeasureSpecMode public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } } }
可以看出View的靜態內部類MeasureSpec,定義了3常量,UNSPECIFIED和EXACTLY和AT_MOST,這3個都是父容器對子容器的約束等級,同時也能看出getMode和getSize函式能獲取到父容器的大小值和約束型別。
正常情況使用View指定的MeasureSpec進行View的測量,同時可給View設定LayoutParams。LayoutParams在View測量時,會在父容器的約束下轉換成對應MeasureSpec,從而確定View的寬高。
因此,View的MeasureSpec確定(即View的寬高確定)需要父容器的MeasureSpec約束,加上View的LayoutParams才行。下面驗證一下。View的測量由measure函式實現,但是,measure函式呼叫前,都會先呼叫父容器的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) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
else if (childDimension == LayoutParams.WRAP_CONTENT) {
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) {
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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
翻譯如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
獲取限制資訊中的尺寸和模式。
switch (限制資訊中的模式) {
case 當前容器的父容器,給當前容器設定了一個精確的尺寸:
if (子View申請固定的尺寸) {
你就用你自己申請的尺寸值就行了;
} else if (子View希望和父容器一樣大) {
你就用父容器的尺寸值就行了;
} else if (子View希望包裹內容) {
你最大尺寸值為父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
}
break;
case 當前容器的父容器,給當前容器設定了一個最大尺寸:
if (子View申請固定的尺寸) {
你就用你自己申請的尺寸值就行了;
} else if (子View希望和父容器一樣大) {
你最大尺寸值為父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
} else if (子View希望包裹內容) {
你最大尺寸值為父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
}
break;
case 當前容器的父容器,對當前容器的尺寸不限制:
if (子View申請固定的尺寸) {
你就用你自己申請的尺寸值就行了;
} else if (子View希望和父容器一樣大) {
父容器對子View尺寸不做限制。
} else if (子View希望包裹內容) {
父容器對子View尺寸不做限制。
}
break;
} return 對子View尺寸的限制資訊;
}
不難看出,父容器的MeasureSpec和View的LayoutParams確定子View的MeasureSpec,同時View的margin和padding也會有影響。
1) SpecMode
UNSPECIFIED:父容器不對內部view做大小限制
EXACTLY:父容器已檢測出view的精確大小,此時view大小即為SpecSize所指定的值。此情況對應為view設定了match_parent或者指定值大小的LayoutParams這兩模式
AT_MOST:父容器指定最大值,View的大小不能超出。此情況對應於View設定了wrap_content的LayoutParams。
3.View的工作流程
如果是view,measure函式就可以完成測量過程。如果是viewGroup,除了自身測量外,同時也會去遍歷子元素的measure函式。
1) View的measure過程
常量方法measure內部呼叫onMeasure方法,只需看onMeasure的實現。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
測量完大小之後都必須呼叫setMeasuredDimension()方法來儲存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;
}
可以看出AT_MOST和EXACTLY父佈局限制條件下,getDefaultSize()返回的就是MeasureSpec中測量大小過的SpecSize,注意的是這裡只是返回測量大小,View真正大小是在layout流程中決定的。但是幾乎所有情況下測量過後的大小和最終大小是相等的。而UNSPECIFIED父佈局限制條件下的測量大小為getSuggestedMinimumWidth()/getSuggestedMinimumHeight()的返回值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
返回值與View是否設定背景相關。
2)ViewGroup的measure過程
ViewGroup通過measureChild(int widthMeasureSpec, int heightMeasureSpec)遍歷測量子元素大小
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 child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看出measureChild()方法,拿到子元素的LayoutParams,再根據父容器的MeasureSpec和padding值,然後去生成子元素的MeasureSpec,最終交由view的measure處理。
3)layout流程
當ViewGroup位置確定後,會在onLayout方法中遍歷子元素的layout()方法去決定子元素位置,看下layout()原始碼
public void layout(int l, int t, int r, int b) {
...
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//又重新呼叫onLayout,決定子元素在viewgroup中的位置
onLayout(changed, l, t, r, b);
...
}
...
}
layout方法中先通過setFrame()方法確定子View的四個頂點位置,又重新呼叫onLayout,確定子元素在viewgroup中的位置。
4)draw流程
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, 畫背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2和5步驟跳過
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 畫內容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, 畫子元素view。遍歷呼叫子元素view的draw方法
dispatchDraw(canvas);
// Step 6, 畫裝飾 (如scrollbars的滾動條)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
return;
}
...
}
可以看出繪製分為6步,其中第二步和第五步通常都是跳過的。我們只看剩下的四步:
- 繪製背景
- 繪製自己的內容(onDraw())
- 繪製子view(dispatchDraw())
- 繪製裝飾
細節還是很多的,其中dispatchDraw()會遍歷所有子元素的draw方法,使得draw事件一層層的傳遞。