在Android體系中View作為視覺(jué)上的呈現(xiàn)挂据,扮演著非常重要的角色娃弓。盡管Android提供了一套包含很多控件的GUI庫(kù)雾狈。但是在大多數(shù)情況下烛恤,因?yàn)榻换セ蛘宫F(xiàn)的定制化要求母怜,我們不是不能直接拿來(lái)使用的。怎么解決這一問(wèn)題呢缚柏?
為了防止Android應(yīng)用界面的同類化嚴(yán)重苹熏,我們需要通過(guò)自定義View來(lái)實(shí)現(xiàn)更友好的用戶體驗(yàn)和更美觀的呈現(xiàn)效果。
前言
為了更好的自定義View币喧,我們應(yīng)該掌握View的基本流程轨域,如:View的測(cè)量流程、布局流程及繪制流程粱锐;
除了三大流程之外疙挺,View常見(jiàn)的回調(diào)方法也是需要熟練掌握的。
- 構(gòu)造方法:
基本屬性和自定義屬性的初始化和賦值怜浅; - onAttach
- onVisibilityChanged
- onDetach
View的工作原理
ViewRoot對(duì)應(yīng)于ViewRootImpl類铐然,它是連接WindowManger和DecorView的紐帶,View的三大流程均是通過(guò)ViewRoot來(lái)完成的恶座。
在ActivityThread中搀暑,當(dāng)Activity對(duì)象創(chuàng)建完畢后,會(huì)將DecorView添加到Window中跨琳,同時(shí)創(chuàng)建ViewRootImpl對(duì)象自点,并將ViewRootImpl對(duì)象和DecorView建立關(guān)聯(lián)。參看下面源碼(位于android-25/android/app/ActivityThread.java
):
//該代碼位于handleLaunchActivity->handleResumeActivity方法中
ActivityClientRecord r = mActivities.get(token);
...
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
View的繪制流程是從ViewRoot的performTraversals方法開(kāi)始的脉让,經(jīng)過(guò)measure桂敛、layout和draw三個(gè)過(guò)程才最終將一個(gè)View繪制出來(lái)功炮。其中:
- measure:用來(lái)測(cè)量View的寬和高;
- layout:用來(lái)View在父容器放置的位置术唬;
- draw:負(fù)責(zé)將View繪制在屏幕上薪伏;
如圖所示,performTraversals會(huì)依次調(diào)用performMeasure粗仓、performLayout和performDraw三個(gè)方法嫁怀,這三個(gè)方法分別完成頂級(jí)View的measure、layout和draw這三大流程借浊;其中performMeasure中會(huì)調(diào)用measure方法塘淑,measure方法又會(huì)調(diào)用onMeasure方法,在onMeasure方法則會(huì)對(duì)所有的子View進(jìn)行measure過(guò)程蚂斤,這時(shí)measure流程就從父容器傳遞到子元素中去存捺。然后子元素重復(fù)父容器的measure過(guò)程,如此反復(fù)就完成了整個(gè)View樹(shù)的遍歷曙蒸,這樣就完成了依次measure過(guò)程召噩。另外兩個(gè)過(guò)程是類似的,不贅述。
Measure過(guò)程
measure過(guò)程決定了View的寬高逸爵,measure完成以后可以通過(guò)getMeasureWidth和getMeasureHeight方法來(lái)獲取View測(cè)量后的寬高,幾乎所有情況下這兩個(gè)值都等同于View的最終寬高值凹嘲,特殊情況下除外(onLayout時(shí)強(qiáng)制指定寬高)师倔。
Layout過(guò)程
layout過(guò)程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的View寬高。方法完成后周蹭,可以通過(guò)getTop趋艘、getBottom、getLeft凶朗、getRight來(lái)拿到View四個(gè)頂點(diǎn)的位置瓷胧,并可以通過(guò)getWidth、getHeight方法來(lái)拿到View最終寬高棚愤。
Draw過(guò)程
draw過(guò)程決定了View的顯示內(nèi)容搓萧,只有draw方法完成之后View的內(nèi)容才能顯示在屏幕上。
DecorView層級(jí)組成
DecorView作為頂級(jí)的View宛畦,一般情況下它內(nèi)部會(huì)包含一個(gè)豎直方向的LinearLayout瘸洛,這個(gè)LinearLayout里面有上下兩部分(具體情況和系統(tǒng)版本及主題有關(guān)),上面是標(biāo)題欄次和,下面是內(nèi)容區(qū)反肋。
Activity通過(guò)setContentView所設(shè)置的布局其實(shí)是添加到DecorView的內(nèi)容區(qū)里面了。而內(nèi)容區(qū)的id是content踏施,因此可以理解制定布局的方式不叫setView而叫setContentView石蔗;確切的說(shuō)罕邀,設(shè)置的布局是加在id為content的FrameLayout的布局中。
如圖:
理解MeasureSpec
MeasureSpec通過(guò)將高2位的SpecMode和低30位的SpecSize打包成一個(gè)32位的int里面來(lái)避免過(guò)多的內(nèi)存分配养距。為了方便操作诉探,其同時(shí)提供了打包和解包方法。
SpecMode
UNSPECIFIED(未指明模式)
父容器不對(duì)View做任何限制铃在,要多大給多大阵具,這種情況下一般用于系統(tǒng)內(nèi)部,表示一種測(cè)量的狀態(tài)定铜。EXACTLY(準(zhǔn)確模式)
父容器已經(jīng)檢測(cè)出View所需要的精確大小阳液,這時(shí)View的最終大小就是低30位的SpecSize指定的值。這種情況對(duì)應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種模式揣炕。AT_MOST(最大模式)
父容器指定了一個(gè)可用大小即就是低30位的SpecSize的值帘皿,最終View的值不能大于這個(gè)值。它對(duì)應(yīng)于LayoutParmas中的wrap_content畸陡。
MeasureSpec和LayoutParams的對(duì)應(yīng)關(guān)系
Android系統(tǒng)內(nèi)部是通過(guò)MeasureSpec來(lái)進(jìn)行View測(cè)量的鹰溜,但正常情況下我們使用View指定MeasureSpec;同時(shí)我們也可以給View設(shè)置LayoutParams丁恭。在View的測(cè)量時(shí)曹动,系統(tǒng)會(huì)將LayoutParams在父容器的約束下轉(zhuǎn)換成對(duì)應(yīng)的MeasureSpec,然后在根據(jù)MeasureSpec來(lái)確定View測(cè)量后的寬高牲览。
- MeasureSpec不是僅由LayoutParams決定的墓陈,其需要和父容器一起才能決定View的MeasureSpec,從而進(jìn)一步?jīng)Q定View的寬高。
- 頂級(jí)View(DecorView)的MeasureSepc由窗口的尺寸和其自身的LayoutParams來(lái)共同決定第献。
- 對(duì)于普通View,它的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams來(lái)共同決定贡必。
- MeasureSpec一旦確定onMeasure中就可以確定View的測(cè)量寬、高庸毫。
DecorView中對(duì)應(yīng)關(guān)系
- LayoutParams.MATCH_PARENT:精確模式大小就是窗口的大小沟于。
- LayoutParams.WRAP_CONTENT:最大模式尽纽,大小不確定,但不能超過(guò)窗口的大小
- 固定模式(如,100dp):精確模式来惧,大小為L(zhǎng)ayoutParams指定的大小四苇。
普通View中的對(duì)應(yīng)關(guān)系
- 當(dāng)View采用固定模式寬高的時(shí)候盗冷,不管父容器的MeasureSpec是什么荔烧,View的MeasureSpec是精確模式且其大小為當(dāng)前View要求的大小。
- 當(dāng)View寬高采用match_parent時(shí)刚盈,那么View的MeasureSpec是精確模式且其大小為父容器剩余的空間大邢勐濉;如果父容器為最大模式,那么View的MeasureSpec是最大模式且其大小不超過(guò)父容器剩余的空間大小欲侮。
- 當(dāng)View的寬高采用wrap_content時(shí)崭闲,不論父容器的模式是精確模式還是最大模式,View的MeasureSpec是最大模式且其大小不超過(guò)父容器剩余的空間大小威蕉。
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
View的工作流程
View的工作流程主要是指measure(測(cè)量)刁俭、layout(布局)、draw(繪制)這三大流程韧涨。
其中measure確定了View的測(cè)量寬高牍戚,layout確定了View的最終寬高和四個(gè)頂點(diǎn)的位置,draw則將View繪制到屏幕上虑粥。
measure過(guò)程
measure過(guò)程要分兩種情況來(lái)分析如孝。如果是原始的View,那么通過(guò)measure方法就完成了View的測(cè)量過(guò)程娩贷;如果是一個(gè)ViewGroup除了完成自己的測(cè)量過(guò)程外第晰,還要遍歷調(diào)用所有子元素的measure方法,各個(gè)子元素再遞歸的執(zhí)行這個(gè)流程彬祖。
原始View的measure過(guò)程
View的measure過(guò)程使用measure方法完成的茁瘦,measure方法是一個(gè)final修飾的方法,意味著子類不能重寫(xiě)此方法储笑,在measure方法中會(huì)調(diào)用其onMeasure方法甜熔。onMeasure方法源碼如下:
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;
}
onMeasure方法很簡(jiǎn)潔,setMeasuredDimension方法會(huì)設(shè)置View的寬高的測(cè)量值突倍。我們同時(shí)需要關(guān)注getDefaultSize這個(gè)方法纺非,這個(gè)方法邏輯很簡(jiǎn)單在AT_MOST和EXACTLY這兩種情形下,getDefaultSize大小就是MeasureSpec的specSize的值赘方。
而在UNSPECIFIED(位指明模式)的情形下,一般用于系統(tǒng)內(nèi)部的測(cè)量過(guò)程弱左,getSuggestedMinimumWidth窄陡、getSuggestedMinimumHeight這兩個(gè)方法的返回值。源碼如下:
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
這種情形下拆火,可以得出如下結(jié)論:
如果View沒(méi)有設(shè)置背景跳夭,那么返回android:minWidth這個(gè)屬性所指定的值,這個(gè)值可以為0们镜;如果View設(shè)置了背景币叹,則返回minWidth和背景的最小寬度、高度之間的最大值模狭。
結(jié)論
直接繼承View的自定義空間需要重寫(xiě)onMeasure方法颈抚,并設(shè)置wrap_content時(shí)的自身大小,否則在布局中使用wrap_content就相當(dāng)于使用了match_parent嚼鹉。
View在布局中使用wrap_content,那么他的specMode為AT_MOST,這種模式下寬高等于specSize,根據(jù)之前的表格可以得知,specSize就是parentSize即父容器當(dāng)前剩余的空間大小;很顯然這種效果等同于布局中使用match_parent贩汉。
如何解決這個(gè)問(wèn)題?很簡(jiǎn)單驱富,只需要給View指定一個(gè)默認(rèn)的內(nèi)部寬高,并在wrap_content是設(shè)置此寬高即可匹舞。代碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
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);
}
}
查看TextView褐鸥、ImageView等的源碼可以知道,針對(duì)wrap_content的情形赐稽,在onMeasure方法中都做了特殊處理叫榕,可以自行閱讀查看。
ViewGroup的measure過(guò)程
和View不同的是ViewGroup是一個(gè)抽象類姊舵,因此它沒(méi)有重寫(xiě)onMeasure方法晰绎,而是提供了一個(gè)叫做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);
}
上述代碼可以看出ViewGroup在measure時(shí)寒匙,會(huì)對(duì)每一個(gè)子元素進(jìn)行measure,measureChild方法的實(shí)現(xiàn)就是取出子元素的LayoutParams躏将,然后再通過(guò)getChildMeasureSpec來(lái)獲取子元素的MeasureSpec,接著講MeasureSpec傳遞給依次調(diào)用子元素的measure方法進(jìn)行測(cè)量锄弱。
總結(jié)
ViewGroup作為一個(gè)抽象類并沒(méi)有定義其測(cè)量的具體過(guò)程,代表測(cè)量過(guò)程的onMeasure方法需要各個(gè)子類進(jìn)行具體的實(shí)現(xiàn)祸憋,比如LinearLayout会宪、RelativeLayout等。ViewGroup的實(shí)現(xiàn)類有不同的布局特性蚯窥,導(dǎo)致系統(tǒng)無(wú)法統(tǒng)一實(shí)現(xiàn)掸鹅,因此才決定交給擴(kuò)展類區(qū)實(shí)現(xiàn)。
View的測(cè)量過(guò)程是三大流程中最復(fù)雜的一個(gè)拦赠,measure完成以后巍沙,通過(guò)getMeasureWidth/Height方法就可以正確獲取到View的測(cè)量寬、高荷鼠。需要注意的是句携,極端的情況下,系統(tǒng)可能需要多次調(diào)用measure才能最終確定測(cè)量寬允乐、高矮嫉,這種情形下onMeasure拿到的測(cè)量寬高可能并不準(zhǔn)確。一個(gè)比較好的習(xí)慣是在onLayout方法中獲取View的測(cè)量寬高牍疏,或者最終寬高蠢笋。