我們開發(fā)過(guò)程,基本需要自定義View,畫一些自己的小插件出來(lái)
這需要我們掌握整個(gè)View的繪畫過(guò)程和一些別的小技巧。
這里總結(jié)下整個(gè)View的源碼中涉及到的一些繪制過(guò)程的核心部分,
之后再來(lái)看下整體的內(nèi)容,畢竟整個(gè)源碼有近2W1行,不是隨便一時(shí)半會(huì)能搞定的,還是得下不少功夫。
起航 ------ 繪制流程
API:23
一般View的“生命周期”即繪畫的流程像下面這樣。
st=>start: View的繪畫流程
op=>operation: measure()
op2=>operation: layout()
op3=>operation: draw()
e=>end: 結(jié)束
st->op->op2->op3->e
這個(gè)是一般的流程都這樣,
- 我們的
measure負(fù)責(zé)去測(cè)量View的Width和Height, - 然后我們的
layout負(fù)責(zé)去確定其在父容器的位置, - 最后由
draw來(lái)負(fù)責(zé)在屏幕上畫內(nèi)容。
但實(shí)際還有一些別的步驟流程,如這些函數(shù)由上一層來(lái)調(diào)用, 就像我們的Activity的onCreate等!
不過(guò)現(xiàn)在先不提及。我們繼續(xù)看各個(gè)階段具體到底是做什么先。
measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
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;
}
我們的measure函數(shù)是個(gè)final類型的,里面主要是調(diào)用了onMeasure函數(shù),由他做具體測(cè)量。
這里需要補(bǔ)充一部分內(nèi)容,關(guān)于MeasureSpec.EXACTLY,MeasureSpec.AT_MOST和MeasureSpec.UNSPECIFIED
-
EXACTLY:
這個(gè)詞的意思是父容器已經(jīng)檢測(cè)出View的精確大?。╡g:width=200dp/match_parent),這時(shí)我們的View的最終大小值就是specSize的值。 -
AT_MOST:
這個(gè)詞的意思是父容器指定了一個(gè)大小(eg:width=wrap_content),這時(shí)我們的View的大小是要小于等于specSize的值,最終大小到底是多大,要看View的具體實(shí)現(xiàn)。 -
UNSPECIFIED:
這個(gè)詞的意思是父容器不對(duì)View有任何大小的限制,需要多大就設(shè)置多大,但這一般是系統(tǒng)內(nèi)部用來(lái)表示一種測(cè)量的狀態(tài)。當(dāng)然還有別的用處,例如我們的ScrollView,他就可以用這個(gè)來(lái)告訴子View,大小無(wú)限,任意畫。
上面的解釋看起來(lái)這個(gè)View的MeasureSpec類型由我們的LayoutParams來(lái)設(shè)置,但實(shí)際這個(gè)MeasureSpec是由View和父容器一起決定的。這個(gè)好理解,例如我們的LinearLayout里面有個(gè)View,前者設(shè)置最高為200dp,后者為300dp,最終這個(gè)子View大小不由自己設(shè)置的300dp決定。具體的測(cè)量過(guò)程,下次再開貼說(shuō),就不插在這里了。我們繼續(xù)主線
這樣我們回看上面應(yīng)該就好理解getDefaultSize()里面的到底是什么意思了。
在 MeasureSpec.UNSPECIFIED:的狀況下,大小是result = size;,由傳過(guò)來(lái)的參數(shù)覺得,我們看下具體做了什么
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
我們拿getSuggestedMinimumHeight()來(lái)看下
里面含義就是:
-
如果我沒背景,那么就是mMinHeight大小,這個(gè)值對(duì)應(yīng)于我們寫的
android:minHeight="20dp"屬性,他的默認(rèn)值是0。case R.styleable.View_minWidth: mMinWidth = a.getDimensionPixelSize(attr, 0); break; -
如果我有背景,那就選背景的最小高度和mMinHeight中最大的。
這個(gè)背景的getMinimumHeight()內(nèi)容是/** * Returns the minimum height suggested by this Drawable. If a View uses this * Drawable as a background, it is suggested that the View use at least this * value for its height. (There will be some scenarios where this will not be * possible.) This value should INCLUDE any padding. * * @return The minimum height suggested by this Drawable. If this Drawable * doesn't have a suggested minimum height, 0 is returned. */ public int getMinimumHeight() { final int intrinsicHeight = getIntrinsicHeight(); return intrinsicHeight > 0 ? intrinsicHeight : 0;}
自帶的解釋已經(jīng)很具體了,返回Drawable的最小高度,沒有的話就返回0;可能有些奇怪,說(shuō)得好像我們的Drawable可以沒高是的。確實(shí)有些沒有,例如我們?cè)谧远x一些我們的圓角的Button在不同點(diǎn)擊效果時(shí)候用到的<shape>標(biāo)簽寫的背景,他就沒有。
繼續(xù)回主線
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
最后就設(shè)置了測(cè)量的大小,是的測(cè)量的大小,不是最終的大小,最終的大小還是需要根據(jù)實(shí)際做調(diào)整的。
這樣我們的measure,測(cè)量過(guò)程就基本結(jié)束了。
一些題外話:
這里補(bǔ)充一個(gè)早年無(wú)知時(shí)候遇到的坑,那時(shí)候項(xiàng)目要求弄一個(gè)像下面這樣的一個(gè)帶有氣泡框的進(jìn)度條,
那時(shí)候就直接類似于下面這樣,繼承View,然后重寫onDraw函數(shù),在里面繪制好整個(gè)樣子。
public class BubbleProgressBar extends View {
public void onDraw(@NonNull Canvas canvas) {
//畫進(jìn)度和泡泡框
}
}
但畫好后,遇到個(gè)問題,這個(gè)View居然自動(dòng)填充滿整個(gè)界面,我設(shè)置的是wrap_content,感覺應(yīng)該是系統(tǒng)幫我搞好,弄成很小的一個(gè)啊,怎么就那么大呢? 后來(lái)查了資料發(fā)現(xiàn),如果我們是直接繼承于View,那需要重寫下那個(gè)measure函數(shù),要不然他就會(huì)自動(dòng)填滿,為啥呢?回看那個(gè)getDefaultSize函數(shù)
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
我們的wrap_content就是那個(gè)AT_MOST和EXACTLY是同條路,實(shí)際就等于寫了Match_parent。
所以我們得根據(jù)情況來(lái)做判斷,來(lái)給點(diǎn)指定大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(heightMode==MeasureSpec.AT_MOST widthMode == MeasureSpec.AT_MOST ){
setMeasuredDimension(mOurDefalutHeight,mOurDefalutWidth);
}
...
}
現(xiàn)在想想,大概當(dāng)年設(shè)計(jì)這個(gè)View類的人遇到了這個(gè)默認(rèn)初始化大小應(yīng)該是多大才合適的問題,所以干脆直接來(lái)個(gè)填充全局的方式。
前進(jìn) ------ Layout過(guò)程
看完了測(cè)量出界面的大小,我們需要開始下一步layout的過(guò)程了。
layout主要是用來(lái)確定View的位置的,具體如下
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(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);
}
}
}
...
}
整個(gè)流程大致是先用setFrame()函數(shù)去設(shè)置我們的View的位置,然后調(diào)用onLayout來(lái)讓服從其確定之元素的位置,由于這個(gè)onLayout做的是具體的布局工作,需要具體的繼承的人去做,例如我們的LinearLayout有水平和垂直之分,所以在View中里面什么也沒有。最后是調(diào)用監(jiān)聽函數(shù),通知他們onLayoutChange()了。
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;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
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;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
}
return changed;
}
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override this method
* and call layout on each of their children.
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
好了,基本的layout過(guò)程就這么結(jié)束了,我們的View的布局也就確定了。
接下來(lái)就看下draw過(guò)程了。
前進(jìn)前進(jìn)------畫界面的Draw
這個(gè)畫的過(guò)程,主要就是把View繪制到屏幕上去,根據(jù)寫的注釋,我們看到View的繪制過(guò)程有這里六個(gè)步驟。其中兩個(gè)可以忽略的。
/*
* 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)
*/
繼續(xù)的步驟如下:
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;
// 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
... 畫特效部分
}
我們?cè)偌?xì)看下各個(gè)步驟
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
...
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);
}
}
然后這個(gè)onDraw和我們onLayout一樣,需要自己寫,里面空空如也
protected void onDraw(Canvas canvas) {
}
然后那個(gè)dispatchDraw()也是,這個(gè)需要我們自己做,但這個(gè)更多的是針對(duì)于ViewGroup類的包含子View的。這樣Draw事件就傳遞給下面,遍歷所有的子View元素的Draw方法,繪制完所有。
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}
這樣我們的Draw過(guò)程也就介紹了。
一些補(bǔ)充
看完一個(gè)完整的View的繪制過(guò)程,這里補(bǔ)充一些關(guān)于ViewGroup的內(nèi)容
ViewGroup繪制過(guò)程中還需要讓他的各個(gè)子View去繪制。
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);
}
}
}
這里看到,他對(duì)于那些除了設(shè)置為Gone不可見的,都進(jìn)行了繪制。
不過(guò)有一個(gè)點(diǎn)引起我的興趣,這個(gè)size的大小不是取數(shù)組children的大小,而是mChildrenCount這個(gè)值。難道這背后有一個(gè)什么故事?查了下沒什么結(jié)果。。。
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);
}
繪制的過(guò)程也是直接調(diào)用他們的measure函數(shù)去執(zhí)行。在獲取到子View的MeasureSpec時(shí),具體是:
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這里面做的事情,主要的就是根據(jù)父容器的MeasureSpec同時(shí)結(jié)合View本身的LayoutParams來(lái)共同決定子View的MeasureSpec,所以子元素能用的大小就是父容器的尺寸減去padding
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
前面在說(shuō)View的時(shí)候也有提到過(guò)這個(gè),具體的View的大小是需要和父容器協(xié)商的。
根據(jù)上面的內(nèi)容的決定子View的大小的過(guò)程,我們可以總結(jié)出一個(gè)規(guī)律,就是如果我們?cè)O(shè)置了具體的大?。╠p/px)那就是ChildSize,要不然是ParentSize除了UNPSECIFIED,
| childParams \ parentParams | EXACTLY | AT_MOST | UNSPECIFIED |
|---|---|---|---|
| dp/px | EXACTLY - childSize | EXACTLY - childSize | EXACTLY - childSize |
| match_parent | EXACTLY - parentSize | EXACTLY - parentSize | UNSPECIFIED - 0 |
| wrap_content | EXACTLY - parentSize | EXACTLY - parentSize | UNSPECIFIED - 0 |
后記
一個(gè)View的繪制過(guò)程就這樣結(jié)束了,也沒太大負(fù)責(zé)的內(nèi)容,但一個(gè)View里面的內(nèi)容還是很多可以說(shuō)的,
例如:
- 他內(nèi)部的
post機(jī)制,他可以讓我們減少對(duì)Handler的使用。 - Touch事件的傳遞
- View的滑動(dòng)
這些內(nèi)容我們后面繼續(xù)慢慢的補(bǔ)充吧。
另外這個(gè)View的調(diào)用者是ViewRoot,他的具體實(shí)現(xiàn)是ViewRootImpl,在他的performTraversal函數(shù)里面,執(zhí)行了我們的view的整個(gè)繪制周期的調(diào)用
st=>start: performTraversals()
e=>end: 結(jié)束
op1=>operation: View.measure
op2=>operation: View.layout
op3=>operation: View.draw
cond1=>condition: 不用重新Measure?
cond2=>condition: 不用重新Layout?
cond3=>condition: 不用重新Draw?
st->cond1->cond2
cond1(yes)->cond2
cond1(no)->op1
cond1->cond2->cond3
cond2(yes)->cond3
cond2(no)->op2
cond3(yes)->e
cond3(no)->op3
更具體的調(diào)用流程如下:
performMeasure->measure:
measure->onMeasure:
onMeasure--> View.measure:
另外我們的layout和draw的套路類似,就不細(xì)寫.