1, View三部曲
在oncreate方法中加載解析完xml資源建立view對象之後,Activity中的makeVisible方法會将這些對象依次測量,确定位置并且顯示在幕布上。總體的流程圖如下,
重點分析onMeasure,onLayout,和onDraw方法。
1.1 onMeasure
measure是測量的意思,那麼onMeasure()方法顧名思義就是用于測量視圖的大小的.
1.1.1 view
首先看沒有子view的測量方法.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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的值由specSize和specMode共同組成的,其中specSize記錄的是大小(後30位),specMode記錄的是規格(前2位). specMode一共有三種類型。
變量widthMeasureSpec和heightMeasureSpec都是ViewRoot的performTraversals方法中擷取的,
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
根視圖總是會充滿全屏的。
1.1.2 ViewGroup
既然子view都實作了onMeasure方法,那麼是不是也有onMeasure方法,并且逐個周遊目前布局下的所有子view呢?
檢視ViewGroup源碼,發現并沒有實作onMeasure方法,但是有一個protected類型的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);
}
}
}
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);
}
看來真的會周遊目前布局下的所有子view,然後調用view的measure方法呢,和上面的猜測一樣。但是真的是這樣的嗎?全局搜尋源碼發現,僅有少數類(ViewGroup的子類)的onMeasure方法會調用measureChildren方法,例如, StackView, AbsoluteLayout等,其他如FrameLayout, LinearLayout, RelativeLayout, AbsoluteLayout等類不會調用該方法。
1.2 onLayout
measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用于給視圖進行布局的,也就是确定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,調用performLayout并調用View的layout()方法來執行此過程,如下所示:
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
layout()方法接收四個參數,分别代表着左、上、右、下的坐标,當然這個坐标是相對于目前視圖的父視圖而言的。可以看到,這裡還把剛才測量出的寬度和高度傳到了layout()方法中。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
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(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
重繪時調用View的onLayout方法,但是onLayout是一個空方法,哪裡出問題了呢? 因為onLayou是為了确定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。是以應該是ViewGroup(未實作)以及其子類會實作該方法。例如, FrameLayout的onLayout方法如下,
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
LayoutParams是子view期望在ViewGroup中的位置,比如,距離子view頂部多少等等資訊,和width等資訊完全不同,不要混淆。
1.3 ondraw
measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這裡才真正地開始對視圖進行繪制。ViewRootImpl中的drawSoftware方法會擷取一個Canvas對象,然後調用view的draw方法,
mView.draw(canvas);
view的draw方法主要分為6步, 第二步和第五步在一般情況下很少用到,是以僅分析剩下的4個步驟,
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas); // 繪制背景
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas); // 繪制view
// Step 4, draw the children
dispatchDraw(canvas); // 繪制子view
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas); // 繪制前景
1.3.1 drawBackground
繪制view的背景
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
這裡會先得到一個Drawable對象,然後根據layout過程确定的視圖位置來設定背景的繪制區域,之後再調用Drawable的draw()方法來完成背景的繪制工作。那麼這個mBackground對象是從哪裡來的呢?其實就是在XML中通過android:background屬性設定的圖檔或顔色。當然也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進行指派。
1.3.2 onDraw
onDraw方法會對視圖的内容進行繪制,View的onDraw方法是一個空方法, 因為每個視圖的内容部分肯定都是各不相同的,這部分的功能交給子類來去實作也是理所當然的。TextView、ImageView等類的源碼,它們都有重寫onDraw方法,并且在裡面執行了相當不少的繪制邏輯。繪制的方式主要是借助Canvas這個類,它會作為參數傳入到onDraw方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪制任意的東西.
1.3.3 dispatchDraw
View的dispatchDraw又是個空方法,一般包含子view的ViewGroup才會實作該方法。在這個方法中,會調用drawChild,逐個繪制子view
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
1.3.4 onDrawForeground
繪制前景和繪制背景幾乎是一模一樣的,都是調用Drawable的draw方法完成的。