做為一名Android開發(fā)者,自定義View應(yīng)該是我們工作中繞不開的話題片部,畢竟系統(tǒng)提供的View有限霜定,有時很難滿足我們的需求望浩,此時就需要結(jié)合具體的場景來編寫自定義View,通過自定義View不僅可以實現(xiàn)特定的效果缘回,還可以簡化代碼典挑。自定義View的過程搔弄,也是我們自己造小輪子的過程,說不定在你的其它項目中就可以用到倒庵,對提高生產(chǎn)力還是大大有幫助的炫刷。
雖說自定義View是工作中繞不開的話題浑玛,但也是Android中最容易把開發(fā)者繞進(jìn)去的知識點,經(jīng)歷過了從入門到放棄的再重新入門的辛酸過程极阅,也該是用正確的姿勢學(xué)習(xí)自定義View了筋搏。
View是什么呢厕隧?......就是Android中所有控件的基類俄周,我們經(jīng)常聽到的ViewGroup也是View的一個子類峦朗。
直接上來就說如何自定義View未免有些空泛排龄,所以我們從View底層的工作原理開始聊起橄维,知其然也要知其所以然。App中我們所用到看到的一個個控件迄埃,都要經(jīng)過measure(測量)兑障、layout(布局)流译、draw(繪制)三大流程才會呈現(xiàn)在我們的眼前。其中measure用來測量View的大小叠赦,layout用來確定被測量后的View最終的位置革砸,draw則是將View渲染繪制出來算利。其實View的工作流程在我們生活中也能找到類似的原型,比如暂吉,我們要畫一個西瓜缎患,首先要確定西瓜的大小挤渔,接下來要確定畫在紙上的那個位置,最后才進(jìn)行繪制低散。
在分析View的工作流程前熔号,我們先要了解一個重要的知識點---MeasureSpec鸟整,MeasureSpec代表一個View的測量規(guī)格篮条,它是一個32位的int值,高兩位代表測量模式(SpecMode)赴恨,低30位代表在對應(yīng)測量模式下的大邪樗ā(SpecSize)钳垮。通過MeasureSpec的getMode()、getSize()方法可以得到對應(yīng)View寬\高的測量模式以及大小歧焦。
SpecMode肚医,即測量模式有以下三種:
- EXACTLY:父容器已經(jīng)檢測出View所需要的精確大小肠套,此時View的大小就是SpecSize,對應(yīng)于LayoutParams中的具體數(shù)值和match_parent兩種類型舵稠。
- AT_MOST:父容器指定了一個可用的大小即SpecSize哺徊,View的大小由其具體的實現(xiàn)決定乾闰,但不能大于SpecSize涯肩,對用于LayoutParams中的wrap_content巢钓。
- UNSPECIFIED:父容器不對View大小做限制症汹,View需要多大就給多大贷腕,這種測量模式一般用于系統(tǒng)內(nèi)容泽裳,在我們自定義View中很少用到。
View的MeasureSpec是如何確定的呢胸囱?其實是由View自身的LayoutParams和父容器的MeasureSpec共同決定的旺矾。具體的細(xì)節(jié)我們來看源碼夺克,在ViewGroup中有一個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);
}
在measureChildWithMargins()方法中铺纽,通過getChildMeasureSpec()方法得到了View寬\高對應(yīng)的測量模式childWidthMeasureSpec 狡门、childHeightMeasureSpec,接下來重點看getChildMeasureSpec()的實現(xiàn)細(xì)節(jié):
/**
* @param spec 父View寬/高的測量規(guī)格
* @param padding 父View在寬/高上已經(jīng)占用的空間大小
* @param childDimension 子View的寬/高
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);//得到父View在寬/高上的測量模式
int specSize = MeasureSpec.getSize(spec);//得到父View在對應(yīng)測量模式下的寬/高
int size = Math.max(0, specSize - padding);//計算子View在寬/高上可用的空間大小
int resultSize = 0;
int resultMode = 0;
// 開始根據(jù)父View的測量規(guī)格以及子View的LayoutParams判斷子View的測量規(guī)格
switch (specMode) {
// 當(dāng)父View的測量模式為精確的大小時(包括具體的數(shù)值和match_parent兩種)
case MeasureSpec.EXACTLY:
// 如果子View的LayoutParams的寬/高是固定的數(shù)值,那么它的測量模式為MeasureSpec.EXACTLY叛复,
// 大小為LayoutParams對應(yīng)的寬/高數(shù)值褐奥,這樣測量規(guī)格就確定了。
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 如果子View的LayoutParams的寬/高為match_parent儿倒,那么子View的寬/高和父View尺寸相等夫否,即為size,
// 因為父View的尺寸已經(jīng)確定汞幢,則子View的測量模式為MeasureSpec.EXACTLY溉瓶。
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
// 如果子View的LayoutParams的寬/高為wrap_content堰酿,就是說子View想根據(jù)實現(xiàn)方式來自己確定自己的大小张足,
// 這個當(dāng)然可以为牍, 但是寬/高不能超過父View的尺寸,最大為size抖韩,則對應(yīng)的測量模式為MeasureSpec.AT_MOST茂浮。
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父View的測量模式為最大模式時席揽,即父View目前也不知道自己的具體大小谓厘,但不能大于size
case MeasureSpec.AT_MOST:
// 既然子View的寬/高已經(jīng)確定,雖然父View的尺寸尚未確定也要優(yōu)先滿足子View属桦,
// 則子View的寬/高為自身大小childDimension地啰,對應(yīng)的測量模式為MeasureSpec.EXACTLY讲逛。
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 如果子View的LayoutParams的寬/高為match_parent盏混,雖說父View的大小為size,但具體的數(shù)值并不能確定止喷,
// 所以子View寬/高不能超過父View的最大尺寸弹谁,即size,
// 此時子View的寬高為最大為size沟于,則對應(yīng)的測量模式為MeasureSpec.AT_MOST
else if (childDimension == LayoutParams.MATCH_PARENT)
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
// 如果子View的LayoutParams的寬/高為wrap_content旷太,即子View想自己來決定自己的大小供璧,這個當(dāng)然可以
// 同理冻记,因為父View尺寸的不確定性冗栗,所以子View最終自我決定的尺寸不能大于size,
// 對應(yīng)的測量模式為MeasureSpec.AT_MOST偶房。
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 這種情況下棕洋,View的尺寸不受任何限制乒融,主要用于系統(tǒng)內(nèi)部赞季,在我們?nèi)粘i_發(fā)中幾乎用不到,就不分析了次绘。
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;
}
//根據(jù)最終子View的測量大小和測量模式得到相應(yīng)的測量規(guī)格邮偎。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
所以View的測量規(guī)格是由自身的LayoutParams和父View的MeasureSpec共同決定的禾进,它們之間具體的組合關(guān)系如下圖:
既然搞清楚了MeasureSpec是怎么回事泻云,接下來具體來看一下View的工作流程measure宠纯、layout、draw娇哆。
1勃救、measure
首先測量過程要區(qū)分是View還是ViewGroup蒙秒,如果只是單純的View宵统,則是需要測量自身就好了马澈,如果是ViewGroup則需要先測量自身,再去遞歸測量所有的的子View勤婚。
1.1馒胆、當(dāng)自定義View是單純的View時
在View類中有如下方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}
View通過該方法來進(jìn)行大小測量祝迂,但是這是final方法器净,我們并不能重寫,但是在它內(nèi)部調(diào)用了View類的另外一個方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
好熟悉的感覺,這就是我們在自定義View的時候通常重寫的onMeasure()方法四啰。
其中setMeasuredDimension()方法宁玫,用來存儲測量后的View的寬/高,存儲之后柑晒,我們才可以調(diào)用View的getMeasuredWidth()欧瘪、getMeasuredHeight()的到對應(yīng)的測量寬/高。
重點看一下其中的getDefaultSize()方法:
/**
* @param size View的默認(rèn)尺寸
* @param measureSpec View的測量規(guī)格
*/
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;
}
可以發(fā)現(xiàn)匙赞,當(dāng)View的測量模式為MeasureSpec.AT_MOST佛掖、MeasureSpec.EXACTLY時涌庭,它最終的測量尺寸都為specSize 芥被,竟然相等。再結(jié)合上邊的MeasureSpec關(guān)系圖對比下坐榆,可以看到當(dāng)View最終的測量規(guī)格為MeasureSpec.AT_MOST時拴魄,其最終的尺寸為父View的尺寸。所以當(dāng)自定義View在布局中的使用wrap_content和match_parent時的效果是一樣的席镀,View都將占滿父View剩余的空間匹中,但這并不是我們愿意看到的,所以我們需要在View的布局寬/高為wrap_content時豪诲,重新計算View的測量尺寸顶捷,其它情況下直接使用系統(tǒng)的測量值即可,重新測量的模板代碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//View的布局參數(shù)為wrap_content時屎篱,需要重新計算的View的測量寬/高
int measureWidth = 0;
int measureHeight = 0;
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//確定measureWidth服赎、measureHeight
setMeasuredDimension(measureWidth, measureHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
//確定measureWidth
setMeasuredDimension(measureWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
//確定measureHeight
setMeasuredDimension(widthSpecSize, measureHeight);
}
}
至于如何確定measureWidth、measureHeight的值交播,就需要結(jié)合具體的業(yè)務(wù)需求了重虑。
1.2、當(dāng)自定義View是一個ViewGroup時
ViewGroup是一個抽象類秦士,繼承與View類缺厉,但它沒有重寫onMeasure()方法,所以需要ViewGroup的子類去實現(xiàn)onMeasure()方法以進(jìn)行具體測量伍宦。既然View類對onMeasure()方法方法做了統(tǒng)一的實現(xiàn)芽死,為什么ViewGroup類沒有呢?因為View類不牽扯子View的布局次洼,而ViewGroup中的子View可能有不同的布局情況关贵,實現(xiàn)細(xì)節(jié)也有差別,所以無法做統(tǒng)一的處理卖毁,只能交給子類根據(jù)業(yè)務(wù)需求來重寫揖曾。
在ViewGroup類里有一個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);
}
}
}
該方法通過遍歷子View落萎,進(jìn)而調(diào)用measureChild()方法得到子View的測量規(guī)格:
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);
}
可以看到該方法和我們上邊分析的measureChildWithMargins()方法類似,都是通過getChildMeasureSpec()完成對子View規(guī)格的測量炭剪。所以一般情況下练链,我們自定義View如果繼承ViewGroup,則需要在重寫onMeasure()方法時首先進(jìn)行measureChildren()操作來確定子View的測量規(guī)格奴拦。
在1.1中我們提到媒鼓,如果View在布局中寬/高為wrap_content時,需要重寫onMeasure()错妖,來重新計算View的測量寬/高绿鸣,同樣的道理,當(dāng)我們自定義的View是一個ViewGroup的話也需要重新計算ViewGroup的測量寬/高暂氯,當(dāng)然這里的計算一般要考慮子View的數(shù)量以及測量規(guī)格等情況潮模。
2、layout
layout的作用是來確定View本身的位置痴施,在View類中源碼如下:
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;
}
其中有這么一段:
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
用來設(shè)置View四個邊的位置擎厢,即mLeft、mTop辣吃、mBottom动遭、mRight的值,這樣也就確定了View本身的位置齿尽。
接下來通過onLayout(changed, l, t, r, b);
來確定子View在父View中的位置:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
是個空方法哦沽损,所以當(dāng)我們自定義ViewGroup時需要重寫onLayout()方法灯节,來確定子View的位置循头。View類中的layout方法有這么一段注釋:
Derived classes should not override this method.
Derived classes with children should override onLayout.
In that method, they should call layout on each of their children.
大概的意思是這樣的,View的派生類一般不需要重寫layout方法炎疆,應(yīng)該在其派生類中重寫onLayout()方法卡骂,并在onLayout()方法中調(diào)用layout()方法來確定子View的位置。
其實形入,這也符合我們平時自定義View時如果繼承ViewGroup時的情況全跨,我們一般都會重寫onLayout()方法,然后通過layout()方法確定子View的具體位置亿遂。
當(dāng)我們自定義的View如果繼承View類的話浓若,一般就不需要重寫onLayout()方法了哦,畢竟沒有子View么蛇数。
執(zhí)行完layout方法后挪钓,我們的View具體位置也就確定了,此時可以通過getWidth()耳舅、getHeight()方法得到View的最終寬/高:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
還記得我們在分析measure過程時提到碌上,通過getMeasuredWidth()、getMeasuredHeight()可以得到View的測量寬/高,這兩組方法有什么區(qū)別呢馏予?其實一般情況下View的測量寬/高和最終的寬/高相等天梧,只是賦值的時間點不同,但在某些特殊的情況下就有差別了霞丧。拿getWidth()方法來說呢岗,它的返回值是mRight - mLeft,即View右邊位置和左邊位置的差值蛹尝,我們假設(shè)一個自定義ViewGroup中某個子View的四邊的位置分別為:l敷燎、t、r箩言、b硬贯,一般情況下我們會這樣確定子View的位置:
childView.layout(l, t, r, b);
這種情況View的測量寬度和最終寬度是相等的,但如果按照如下的寫法:
childView.layout(l, t, r + 100, b);
此時View的最終寬度會比測量寬度大100px的陨收。在measure過程中有一點需要注意饭豹,如果View的結(jié)構(gòu)比較復(fù)雜,則可能需要多次的進(jìn)行測量才能得到最終的測量結(jié)果务漩,這也會導(dǎo)致我們得到的測量尺寸不準(zhǔn)確拄衰。所以,所以要得到View最終的正確尺寸饵骨,應(yīng)該通過getWidth()或者getHeight()方法翘悉。
3、draw
經(jīng)歷了measure居触、layout的過程妖混,View的尺寸和位置已經(jīng)確定,接下來就差最后一步了轮洋,那就是draw制市,具體的繪制流程是什么樣的呢?查看一下View類中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, 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;
}
//此處省略N行代碼......
}
繪制的流程很清晰弊予,基本按照如下幾個步驟:
- 1祥楣、Draw the background 繪制View的背景
drawBackground(canvas)
- 2、If necessary, save the canvas' layers to prepare for fading 保存畫布層汉柒,準(zhǔn)備漸變
- 3误褪、Draw view's content 繪制內(nèi)容,也就是View自身
onDraw(canvas)
- 4碾褂、Draw children 繪制子View
dispatchDraw(canvas)
- 5兽间、If necessary, draw the fading edges and restore layers 繪制漸變,保存圖層
- 6斋扰、Draw decorations (scrollbars for instance) 繪制裝飾物
onDrawForeground(canvas)
我們關(guān)心的是步驟3渡八、4的onDraw()和dispatchDraw()方法啃洋。
先看onDraw()方法:
protected void onDraw(Canvas canvas) {
}
是一個空方法,這也可以理解屎鳍,畢竟不同的View呈現(xiàn)的效果不同宏娄,所以需要子類重寫來實現(xiàn)具體的細(xì)節(jié)。當(dāng)我們自定義View繼承View類時逮壁,通常會重寫onDraw()方法孵坚,來繪制線條或各種形狀、圖案等窥淆。
再看一下View類的dispatchDraw()方法:
protected void dispatchDraw(Canvas canvas) {
}
依然是空方法卖宠,需要子類去重寫,所以ViewGroup類中重寫了dispatchDraw()方法忧饭,遍歷所有的子View扛伍,其中有一行代碼是drawChild(canvas, transientChild, drawingTime);
正是用來繪制子View的,再看下細(xì)節(jié):
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
其中child.draw(canvas, this, drawingTime);
是子View調(diào)用了View類的draw()方法词裤,則子View得到了最終的繪制刺洒。同樣的道理ViewGroup中的所有子View得到繪制。所以當(dāng)我們自定義的View是ViewGroup的子類時吼砂,必要時可以考慮重寫dispatchDraw()方法來繪制相應(yīng)的內(nèi)容逆航。
到這里我們View的工作流程就分析完畢了,掌握這些基本的原理只是第一步渔肩,但也是必須的因俐,繼續(xù)加油吧。