當Android原生控件無法滿足需求時就要自定義View,只有掌握了View的測量過程 (measure)、布局過程(layout)和繪制過程(draw)過程才能自定義出復雜的View。
預備知識
頂層視圖(DecorView)及其所關聯的ViewRoot對象的創建過程,如下圖所示(參考文檔1):
上圖中第9步獲取到的就是頂層視圖decor,第11、12、13步就是將decor傳遞給ViewRoot,這樣ViewRoot就和DecorView建立了關聯。
在第13步中,ViewRoot類的成員函數setView會調用ViewRoot類的另外一個成員函數requestLayout,該函數會對頂層視圖(DecorView)觸發第一次測量過程 (measure)、布局過程(layout)和繪制過程(draw)。接下來就從requestLayout開始分析:
上圖中的第5步會調用ViewRootImpl類的performTraversals方法,performTraversals方法會依次調用performMeasure方法、performLayout方法和performDram方法來完成頂層視圖decor的測量過程 (measure)、布局過程(layout)和繪制過程(draw)。
View的測量過程 (measure)
上圖的第9步會遍歷每一個子View,并且調用子View的measure方法對子View進行測量(即第10步)。非ViewGroup類型的View通過onMeasure方法就完成了其測量過程,而ViewGroup類型的View除了通過onMeasure方法就完成自身的測量過程外,還要在onMeasure方法中完成遍歷子View并且調用子View的measure方法對子View進行測量。
-
非ViewGroup類型的View的測量過程
View的測量時序圖.jpg
對于上面的步驟進行解析一下,第1步執行View類中的measure方法,該方法是一個final方法,這就意味著子類不能從寫該方法,measure方法會調用View類的onMeasure方法,onMeasure方法的實現代碼如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
從上面的代碼就對應上圖中3、4、5、6、7步,先來看第3步對應的View類的getSuggestedMinimumWidth方法的源碼:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
從getSuggestedMinimumWidth的代碼可以看出,當View沒有設置背景,那么getSuggestedMinimumWidth方法的返回值為mMinWidth,而mMinWidth對應于android: minWidth屬性指定的值,如果沒有設置android: minWidth屬性,則mMinWidth默認為0;如果View設置了背景,則getSuggestedMinimumWidth方法的返回值為max(mMinWidth, mBackground.getMinimumWidth()),下面先來看看Drawable類中getMinimumWidth方法的源碼:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
有上面的代碼可知getMinimumWidth返回的是View的背景的原始寬度,如果View的背景沒有原始寬度,就返回0。
現在來總結一下getSuggestedMinimumWidth方法的邏輯,當View沒有設置背景時,getSuggestedMinimumWidth方法的返回值為android: minWidth屬性指定的值,這個值可以為0;當View設置了背景時,getSuggestedMinimumWidth方法的返回值為android: minWidth屬性指定的值與View的背景的最小寬度中的最大值。
現在我們來看一下最關鍵的View類的getDefaultSize方法的源代碼(對應第4步):
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.AT_MOST和MeasureSpec.EXACTLY測量模式,getDefaultSize直接返回測量后的值(所以直接繼承View的自定義控件需要重寫onMeasure方法并且設置wrap_content時的自身大小,否者在布局中使用wrap_content就相當于使用math_parent);對于MeasureSpec.UNSPECIFIED測量模式,一般用于系統內部的測量過程,getDefaultSize返回值為getSuggestedMinimumWidth方法的返回值。對于第5、6步與3、4步類似,這里就不再綴續了。
第7步中View類的setMeasuredDimension方法調用了第8步中View類的setMeasuredDimensionRaw方法,setMeasuredDimensionRaw方法的源碼:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
有上面的代碼可知,View測量后的寬高被保存到View類的成員變量mMeasuredWidth和mMeasuredHeight中了,通過View類的getMeasuredWidth方法和getMeasuredHeight方法獲取的就是mMeasuredWidth和mMeasuredHeight的值,需要注意的是,在某些極端情況下,系統可能需要多次measure才能確定最終的測量寬高,在這種情況下,在onMeasure方法中拿到的測量寬高很可能是不準確的,一個好的習慣是在onLayout方法中去獲取View最終的測量寬高。
-
ViewGroup類型的View的測量過程
ViewGroup的測量時序圖.jpg
ViewGroup并沒有定義其自身測量的具體過程(即沒有onMeasure方法),這是因為ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類去具體實現,所以上面展示了LinearLayout測量流程圖。
上圖第1步執行View類中的measure方法,該方法是一個final方法,這就意味著子類不能從寫該方法,measure方法會調用LinearLayout類的onMeasure方法,onMeasure方法的實現代碼如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
當前分析當LinearLayout的方向是垂直方向的情況,此時會執行LinearLayout類的measureVertical方法:
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
......
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
......
}
......
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
......
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
.....
首先measureVertical方法會遍歷每一個子元素并且執行measureChildBeforeLayout方法對子元素進行測量,measureChildBeforeLayout方法內部會執行子元素的measure方法。在代碼中,變量mTotalLength會是用來存放LinearLayout在豎直方向上的當前高度,每遍歷一個子元素,mTotalLength就會增加,增加的部分主要包括子元素自身的高度、子元素在豎直方向上的margin。
當測量完所有子元素時,就會很容易得到LinearLayout自身的大小,對于豎直的LinearLayout,水平方向的寬度等于最寬元素的寬度加上左右的padding,如果高度采用的是math_content或者具體數值,那么它的高度為父布局的給到的高度或者具體數值,如果高度采用的是wrap_content,那么高度是所有子元素所占用的高度總和加上上下padding 并且 能超過父容器的剩余空間,這個過程對應與resolveSizeAndState的源碼:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
下面我們來看一看LinearLayout類的measureChildBeforeLayout方法是如何對子元素進行測量,該方法的第第4個和第6個參數分別代表在水平方向和垂直方向上LinearLayout已經被其他子元素占據的長度,measureChildBeforeLayout的源碼如下:
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
LinearLayout類的measureChildBeforeLayout方法會調用ViewGroup類的
measureChildWithMargins方法,measureChildWithMargins方法的源碼如下:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup類的measureChildWithMargins方法會調用子元素的measure方法對子元素進行測量,在對子元素測量之前先會通過調用ViewGroup類的getChildMeasureSpec方法得到傳遞給子元素的MeasureSpec(即能給到子元素的空間),從getChildMeasureSpec方法的前二個參數可知,子元素MeasureSpec的創建與父容器的MeasureSpec、父容器的padding、子元素的margin和兄弟元素占用的長度有關。
ViewGroup類的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) {
// 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);
}
ViewGroup類的getChildMeasureSpec方法的邏輯可以通過下表來說明,注意,表中的parentSize是指父容器目前可使用的大小(參考Android開發藝術探索182頁):
childLayoutParams/parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY/childSize | EXACTLY/childSize | EXACTLY/childSize |
MATCH_PARENT | EXACTLY/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
WRAP_CONTENT | AT_MOST/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
ViewGroup類的getChildMeasureSpec方法返回子元素寬高的MeasureSpec,然后將子元素寬高的MeasureSpec作為measure方法的參數。
到此為止,非ViewGroup類型的View的測量過程和ViewGroup類型的View的測量過程已經分析完畢,進行如下總結:
1> 父View會遍歷測量每一個子View(通常使用ViewGroup類的measureChildWithMargins方法),該方法會調用子View的measure方法并且將父布局剩余空間構建的寬高(通過getChildMeasureSpec方法)作為measure方法的參數。
2> 非ViewGroup類型的View自身的測量是在非ViewGroup類型的View的onMeasure方法中進行測量的
3> ViewGroup類型的View自身的測量是在ViewGroup類型View的onMeasure方法中進行測量的
4>直接繼承ViewGroup的自定義控件需要重寫onMeasure方法并且設置wrap_content時的自身大小,否者在布局中使用wrap_content就相當于使用math_parent,具體原因通過上面的表格可以說明。
View的布局過程(layout)
decor的三大流程圖的第16步會遍歷并且調用子元素的layout方法,layout過程比measure過程簡單多了,layout方法用來確定View本身的位置,而onLayout方法用來確定所有子元素的位置。
ViewGroup類型的View和非ViewGroup類型的View的布局過程是不同的,非ViewGroup類型的View通過layout方法就完成了其布局過程,而ViewGroup類型的View除了通過layout方法就完成自身的布局過程外,還要調用onLayout方法去遍歷子元素并且調用子元素的layout方法,各個子View再去遞歸執行這個流程。
-
非ViewGroup類型的View的布局過程
View的布局時序圖.jpg
對上面的時序圖進行一下解析,第1步執行View類的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;
}
由于setOpticalFrame()內部會調用setFrame(),所以最終都是通過setFrame()方法設置布局的位置的。
接下來看下setFrame方法:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
}
return changed;
}
由上面的源碼可知,setFrame方法是用來設定View的四個頂點的位置,即設置mLeft、mTop、mRight、mBottom這四個值,View的四個頂點一旦確定,那么View在父容器中的位置也就確定了。
第3步layout方法接著調用View類的onLayout方法,這個方法的作用是用來確定子元素的位置,由于非ViewGroup類型的View沒有子元素,所以View類的onLayout方法為空。
- ViewGroup類型的View的布局過程
ViewGroup的布局時序圖.jpg
上面其實是LinearLayout的布局時序圖,因為ViewGroup的onLayout方法是抽象方法,所以就選擇了ViewGroup的子類LinearLayout進行分析。
上圖第1步執行ViewGroup類的layout方法,該方法是一個final方法,即子類無法重寫該方法,源代碼如下:
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
第2步ViewGroup類的layout方法會調用View類的layout方法,第3步View類的layout方法調用View類的setFrame方法,這兩步與上面討論非ViewGroup類型的View的布局過程的第1、2步相同,這里就不在贅敘,直接看第4步View類的layout方法調用LinearLayout類的onLayout方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
當前分析LinearLayout的方向是垂直方向的場景,layoutVertical方法如下:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
可以看到onLayout方法會遍歷每一個子元素并且調用setChildFrame方法,setChildFrame方法會調用子元素的layout方法來對子元素進行布局,setChildFrame方法的源碼如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
View的繪制過程(draw)
decor的三大流程圖的第23步會遍歷每一個子View并且調用子元素的draw方法,繼而開始進行子View的繪制過程。先通過如下的時序圖,整體的看一下繪制過程:
上面其實是LinearLayout的繪制時序圖,因為View的onDraw方法是空方法,所以就選擇了ViewGroup的子類LinearLayout進行分析。
LinearLayout的繪制過程遵循如下幾步:
1> 繪制背景
2> 繪制自己(繪制分割線)
3> 繪制子View(dispatchDraw)
4> 繪制前景
Android中是通過View類的draw方法來實現上面的4步,源碼如下所示:
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
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, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
.....
}
從這個方法的注釋可以知道,當自定義View并且需要繪制時,應該重寫View類的onDraw方法而不要重寫View類的draw方法,如果你需要重寫draw方法,必須在重寫時調用父類的draw方法。上面的代碼很明顯的驗證了View繪制過程的4步。由于View類無法確定自己是否有子元素,所以View類的dispatchDraw方法是空方法,那么我們就來看看ViewGroup類的dispatchDraw方法的源碼(由于該方法的源碼太長了,因此我只展示我們感興趣的部分代碼):
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
int transientIndex = transientCount != 0 ? 0 : -1;
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
}
}
......
}
ViewGroup類的dispatchDraw方法會遍歷每一個子元素,然后調用ViewGroup類的drawChild方法對子元素進行繪制,ViewGroup類的drawChild方法源碼如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
自定義View實例
- 自定義View的分類
1> 通過繼承View或者ViewGroup實現自定義View
2> 通過繼承已有的控件實現自定義View
3> 通過組合實現自定義View
我在下面只針對1>來實現自定義View,因為2>和3>相對于1>就比較簡單了。 -
通過繼承View實現環狀進度條
首先展示一下效果圖:
環狀進度條
下面就來分析一下實現代碼:
根據上面對非ViewGrop類型View三大流程的分析,第一步就是測量,
由于是繼承View類的,因此如果想要支持wrap_content屬性,就必須重寫onMeasure方法,如下所示(可以當做模板代碼):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
第二步就是進行布局,由于非ViewGrop類型View自身的布局在View類的layout方法中已經實現,而onLayout方法是用來對子View進行布局的,所以對于非ViewGrop類型View就不用考慮布局的實現。
第三步就是進行繪制,由于非ViewGrop類型View沒有子View,所以不用考慮對子View的繪制,因此只要重寫View類的onDraw方法對自身進行繪制即可,代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(new RectF(getPaddingLeft(), getPaddingTop(), mWidth - getPaddingRight(), mHeight - getPaddingBottom()), 0, sweepValue, false, paint);
}
從上面的代碼中可以看出,如果不在onDraw方法中處理padding,那么padding屬性無法起作用。
-
通過繼承ViewGroup實現流式布局(FlowLayout)
首先展示一下效果圖:
流式布局
下面就來分析一下實現代碼:
根據上面對ViewGrop類型View三大流程的分析,第一步就是測量,
由于是繼承ViewGrop類的,因此如果想要支持wrap_content屬性,就必須重寫onMeasure方法,代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
mLineWidths.clear();
mLineHeights.clear();
mLineViewNums.clear();
int width = 0;
int lineWidth = 0;
int height = 0;
int lineHeight = 0;
int lineViewNum = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (View.GONE == childView.getVisibility()) {
if (i == childCount - 1) {
lineViewNum++;
mLineViewNums.add(lineViewNum);
mLineWidths.add(lineWidth);
width = Math.max(width, lineWidth);
mLineHeights.add(lineHeight);
height += lineHeight;
}
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
if (lineWidth + childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin > widthSpecSize - getPaddingLeft() - getPaddingRight()) {
mLineViewNums.add(lineViewNum);
lineViewNum = 1;
mLineWidths.add(lineWidth);
lineWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
mLineHeights.add(lineHeight);
height += lineHeight;
lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
} else {
lineViewNum++;
lineWidth += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
width = Math.max(width, lineWidth);
lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
}
mLineViewNums.add(lineViewNum);
mLineWidths.add(lineWidth);
mLineHeights.add(lineHeight);
height += lineHeight;
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, height + getPaddingTop() + getPaddingBottom());
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
上面代碼的邏輯:遍歷每一個子元素,然后通過measureChildWithMargins方法對子元素進行測量,注意第3個和第5個參數必須是0,因為我是想在父元素所占有的空間中為子元素進行測量,在遍歷每個子元素的過程中,記錄每一行的最終寬度、最終高度和每一行的子元素個數。
第二步就是進行布局,由于ViewGrop類型View自身的布局在ViewGrop類的layout方法中已經實現,ViewGrop類的layout方法會調用ViewGrop類的onLayout方法,由于ViewGrop類的onLayout方法是抽象的,所以必須實現onLayout方法并且實現對子View的布局,代碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineNum = mLineWidths.size();
int paddingTop = getPaddingTop();
int startIndex = 0;
int endIndex = 0;
for (int line = 0; line < lineNum; line++) {
int paddingLeft = 0;
int currentLineWidth = mLineWidths.get(line);
switch (mGravity) {
case LEFT:
paddingLeft = getPaddingLeft();
break;
case CENTER:
paddingLeft = (getWidth() - currentLineWidth)/2;
break;
case RIGHT:
paddingLeft = getWidth() - currentLineWidth - getPaddingRight();
break;
}
endIndex += mLineViewNums.get(line);
for (; startIndex < endIndex; startIndex++) {
View childView = getChildAt(startIndex);
if (View.GONE == childView.getVisibility()) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int lc = paddingLeft + lp.leftMargin;
int tc = paddingTop + lp.topMargin;
int rc = childView.getMeasuredWidth() + lc;
int bc = childView.getMeasuredHeight() + tc;
childView.layout(lc, tc, rc, bc);
paddingLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
paddingTop += mLineHeights.get(line);
}
}
上面代碼的邏輯:逐行遍歷每一個子View并且計算出子View的左上角和右下角的坐標,然后調用子View的layout方法對子View進行布局。
第三步就是進行繪制,由于我現在設計的流式布局不需要對自己進行繪制,所以不用考慮繪制。