3.4 View詳解
3.10.1 概述
View系統(tǒng)定義了從用戶輸入消息到消息處理的全過(guò)程:用戶通過(guò)觸摸屏或者鍵盤輸入消息后,該消息經(jīng)過(guò)處理后,首先被送到WMS,WMS根據(jù)所有窗口的狀態(tài)(每一個(gè)窗口都是WMS創(chuàng)建的,所以WMS知道所有窗口的信息)判斷用戶正在與哪個(gè)窗口進(jìn)行交互,然后把該消息發(fā)送給該窗口:如果是按鍵消息,則直接發(fā)送給當(dāng)前窗口,如果是觸摸消息,則WMS會(huì)根據(jù)消息的位置坐標(biāo)發(fā)送給相應(yīng)的窗口,最后目標(biāo)窗口怎么處理消息則是應(yīng)用的業(yè)務(wù)邏輯了。
3.10.2 View基本知識(shí)
View的位置參數(shù):top、left、right、bottom,分別對(duì)應(yīng)View的左上角和右下角相對(duì)于父容器的橫縱坐標(biāo)值(平移過(guò)程中是不會(huì)變化的)。此外Android3.0增加了x、y、translationX、translationY四個(gè)參數(shù),其中x和y是View左上角的坐標(biāo),而translationX和translationY是View左上角相對(duì)于父容器的偏移量,默認(rèn)值是0。其關(guān)系如下:x=left+translationX,y=top+translationY。如下圖所示:

MotionEvent是指手指接觸屏幕后所產(chǎn)生的一系列事件,主要有ACTION_UP、ACTION_DOWN、ACTION_MOVE等。正常情況下,一次手指觸屏?xí)|發(fā)一系列點(diǎn)擊事件,主要有下面兩種典型情況:
- 點(diǎn)擊屏幕后離開(kāi),事件序列是ACTION_DOWN->ACTION_UP;
- 點(diǎn)擊屏幕后滑動(dòng)一會(huì)再離開(kāi),事件序列是ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->...->ACTION_UP。
通過(guò)MotionEvent可以得到點(diǎn)擊事件的x和y坐標(biāo),其中g(shù)etX和getY是相對(duì)于當(dāng)前View左上角的x和y坐標(biāo),getRawX和getRawY是相對(duì)于手機(jī)屏幕左上角的x和y坐標(biāo)。
VelocityTracker用于追蹤手指在滑動(dòng)過(guò)程中的速度,包括水平和垂直方向上的速度。具體計(jì)算公式如下所示:
speed = (endPosition - startPosition) / time
從上面公式可知:速度是單位時(shí)間內(nèi)移動(dòng)的像素?cái)?shù)(速度可能為負(fù)值,例如當(dāng)手指從屏幕右邊往左邊滑動(dòng)的時(shí)候),可以使用方法computeCurrentVelocity(xxx)指定單位時(shí)間是多少。例如通過(guò)computeCurrentVelocity(1000)來(lái)獲取速度,手指在1s中滑動(dòng)了100個(gè)像素,那么速度是100,即100(像素/1000ms)。VelocityTracker的使用方式:
//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//獲取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時(shí)候調(diào)用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調(diào)用
GestureDetector用于輔助檢測(cè)用戶的單擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。GestureDetector的使用比較簡(jiǎn)單,主要也是輔助檢測(cè)常見(jiàn)的觸屏事件。如果只是監(jiān)聽(tīng)滑動(dòng)相關(guān)的事件在onTouchEvent中實(shí)現(xiàn),如果要監(jiān)聽(tīng)雙擊這種行為的話,那么就使用GestureDetector。
3.10.3 View的事件體系
View的事件分發(fā)機(jī)制
事件分發(fā)過(guò)程有三個(gè)重要方法:
- dispatchTouchEvent方法用來(lái)進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。
- onInterceptTouchEvent方法在dispatchTouchEvent方法內(nèi)部調(diào)用,用來(lái)判斷是否攔截某個(gè)事件。如果攔截(返回true)則自己消費(fèi)該事件,即調(diào)用自己的onTouchEvent方法,否則傳遞給子view的dispatchTouchEvent方法,由子view處理該事件。需要注意的是,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)再被調(diào)用。
- onTouchEvent方法在dispatchTouchEvent方法內(nèi)部調(diào)用,用來(lái)處理點(diǎn)擊事件,返回true則自己處理該事件,返回false則向上返回讓其父容器處理(調(diào)用其自己的onTouchEvent方法)。如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無(wú)法再次接收到事件(一個(gè)事件序列只能被同一個(gè)view處理)。
三個(gè)方法的關(guān)系可以用下面的偽代碼表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

View的事件分發(fā)體系有以下注意事項(xiàng):
- 當(dāng)一個(gè)點(diǎn)擊事件發(fā)生之后,傳遞過(guò)程遵循如下順序:Activity->Window->View。如果一個(gè)View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法將會(huì)被調(diào)用,依此類推,如果所有的元素都不處理這個(gè)事件,那么這個(gè)事件將會(huì)最終傳遞給Activity處理(調(diào)用Activity的onTouchEvent方法)。
- 正常情況下,一個(gè)事件序列只能被一個(gè)View攔截并消耗,一旦某個(gè)View攔截了某個(gè)事件,那么同一個(gè)事件序列內(nèi)的所有事件都會(huì)直接交給它處理,并且該元素的onInterceptTouchEvent方法不會(huì)被調(diào)用了。某個(gè)View一旦開(kāi)始處理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列的其他事件都不會(huì)再交給它來(lái)處理,并且事件將重新交給它的父容器去處理(調(diào)用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他類型事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,父容器的onTouchEvent方法不會(huì)被調(diào)用,當(dāng)前View依然可以收到后續(xù)的事件,但是這些事件最后都會(huì)傳遞給Activity處理。
- ViewGroup默認(rèn)不攔截任何事件,因?yàn)樗膐nInterceptTouchEvent方法默認(rèn)返回false。View沒(méi)有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,那么它的onTouchEvent方法就會(huì)被調(diào)用。
- View的onTouchEvent默認(rèn)都會(huì)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable都為false)。View的longClickable默認(rèn)是false的,clickable則不一定,Button默認(rèn)是true,而TextView默認(rèn)是false。
- OnTouchListener的優(yōu)先級(jí)比onTouchEvent要高:如果給一個(gè)View設(shè)置了OnTouchListener,那么OnTouchListener中的onTouch方法會(huì)被回調(diào)。這時(shí)事件如何處理還要看onTouch的返回值,如果返回false,那么當(dāng)前View的onTouchEvent方法會(huì)被調(diào)用;否則onTouchEvent方法將不會(huì)被調(diào)用。在onTouchEvent方法中,如果當(dāng)前View設(shè)置了OnClickListener,那么它的onClick方法會(huì)被調(diào)用,所以O(shè)nClickListener的優(yōu)先級(jí)最低。
- View的enable屬性不影響onTouchEvent的默認(rèn)返回值。哪怕一個(gè)View是disable狀態(tài),只要它的clickable或者longClickable有一個(gè)是true,那么它的onTouchEvent就會(huì)返回true。
- 事件傳遞過(guò)程總是先傳遞給父元素,然后再由父元素分發(fā)給子View,通過(guò)requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過(guò)程,但是ACTION_DOWN事件除外,即當(dāng)面對(duì)ACTION_DOWN事件時(shí),ViewGroup總是會(huì)調(diào)用自己的onInterceptTouchEvent方法來(lái)詢問(wèn)自己是否要攔截事件。ViewGroup的dispatchTouchEvent方法中有一個(gè)標(biāo)志位FLAG_DISALLOW_INTERCEPT,這個(gè)標(biāo)志位就是通過(guò)子View調(diào)用requestDisallowInterceptTouchEvent方法來(lái)設(shè)置的,一旦設(shè)置為true,那么ViewGroup不會(huì)攔截該事件。
View的滑動(dòng)沖突
常見(jiàn)的滑動(dòng)沖突的場(chǎng)景:
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致,例如ViewPager中包含listView;
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致,例如ViewPager的單頁(yè)中存在可以滑動(dòng)的bannerView;
- 上面兩種情況的嵌套,例如ViewPager的單個(gè)頁(yè)面中包含了bannerView和listView。
滑動(dòng)沖突處理規(guī)則:可以根據(jù)滑動(dòng)距離和水平方向形成的夾角;或者根據(jù)水平和豎直方向滑動(dòng)的距離差;或者兩個(gè)方向上的速度差等,一般有以下解決方式
- 外部攔截法:點(diǎn)擊事件都先經(jīng)過(guò)父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要就不攔截。該方法需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做相應(yīng)的攔截即可,其他均不需要做修改。偽碼如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要攔截當(dāng)前點(diǎn)擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
- 內(nèi)部攔截法:父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器來(lái)處理。這種方法和Android中的事件分發(fā)機(jī)制不一致,需要配合
requestDisallowInterceptTouchEvent方法才能正常工作。
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {]
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (當(dāng)前View需要攔截當(dāng)前點(diǎn)擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
注:Android開(kāi)發(fā)藝術(shù)探索一書對(duì)這兩種攔截法寫了兩個(gè)例子,感興趣閱讀源碼看下,外部攔截法和內(nèi)部攔截法。
3.10.3 View的滑動(dòng)
常見(jiàn)的實(shí)現(xiàn)View的滑動(dòng)的方式有三種:
- 通過(guò)View本身提供的scrollTo和scrollBy方法:操作簡(jiǎn)單,適合對(duì)View內(nèi)容的滑動(dòng);
- 通過(guò)動(dòng)畫給View施加平移效果來(lái)實(shí)現(xiàn)滑動(dòng):操作簡(jiǎn)單,適用于沒(méi)有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果;
- 通過(guò)改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動(dòng):操作稍微復(fù)雜,適用于有交互的View。
scrollTo和scrollBy方法只能改變View內(nèi)容的位置而不能改變View在布局中的位置,scrollBy是基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo是基于所傳參數(shù)的絕對(duì)滑動(dòng)。通過(guò)View的getScrollX和getScrollY方法可以得到滑動(dòng)的距離。
使用動(dòng)畫來(lái)移動(dòng)View主要是操作View的translationX和translationY屬性,既可以使用傳統(tǒng)的View動(dòng)畫,也可以使用屬性動(dòng)畫。使用動(dòng)畫還存在一個(gè)交互問(wèn)題:在android3.0以前的系統(tǒng)上,View動(dòng)畫和屬性動(dòng)畫,新位置均無(wú)法觸發(fā)點(diǎn)擊事件,同時(shí),老位置仍然可以觸發(fā)單擊事件。從3.0開(kāi)始,屬性動(dòng)畫的單擊事件觸發(fā)位置為移動(dòng)后的位置,View動(dòng)畫仍然在原位置。
彈性滑動(dòng)
- Scroller的工作原理:Scroller本身并不能實(shí)現(xiàn)View的滑動(dòng),它需要配合View的computeScroll方法才能完成彈性滑動(dòng)的效果,它不斷地讓View重繪,而每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔,通過(guò)這個(gè)時(shí)間間隔
Scroller就可以得出View的當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以通過(guò)scrollTo方法來(lái)完成View的滑動(dòng)。就這樣,View的每一次重繪都會(huì)導(dǎo)致View進(jìn)行小幅度的滑動(dòng),而多次的小幅度滑動(dòng)就組成了彈性滑動(dòng)。 - 使用延時(shí)策略來(lái)實(shí)現(xiàn)彈性滑動(dòng),它的核心思想是通過(guò)發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果,具體來(lái)說(shuō)可以使用Handler的sendEmptyMessageDelayed(xxx)或View的postDelayed方法,也可以使用線程的sleep方法。
3.4.3 View的繪制流程
ViewRoot是連接WindowManager和DecorView的紐帶,View的三大流程均通過(guò)ViewRoot來(lái)完成。ActivityThread中,Activity創(chuàng)建完成后,會(huì)將DecorView添加到Window中,同時(shí)創(chuàng)建ViewRootImpl對(duì)象,并建立兩者的關(guān)聯(lián)。
View的繪制流程是從ViewRoot的performTraversals方法開(kāi)始,其會(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ì)所有的子元素進(jìn)行measure過(guò)程,這個(gè)時(shí)候measure流程就從父容器傳遞到子元素了,這樣就完成了一次measure過(guò)程,layout和draw的過(guò)程類似。
measure過(guò)程
measure過(guò)程決定了View的寬高,在幾乎所有的情況下這個(gè)寬高都等同于View最終的寬高。
MeasureSpec
在View測(cè)量的時(shí)候,系統(tǒng)會(huì)將View的LayoutParams在父View的約束下轉(zhuǎn)換成對(duì)應(yīng)的MeasureSpec,然后再根據(jù)這個(gè)MeasureSpec來(lái)確定View測(cè)量后的寬高。對(duì)于DecorView,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams來(lái)決定;對(duì)于普通View,它的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來(lái)共同決定。具體代碼如下所示:
//ViewGroup.java
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);
}
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) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
....
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
由上述代碼,可知普通View的MeasureSpec的創(chuàng)建規(guī)則:
- 當(dāng)View采用固定寬高時(shí),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY,并且大小是LayoutParams中的大小。
- 當(dāng)View的寬高是match_parent時(shí),如果父View的模式是EXACTLY,那么該View也是EXACTLY,并且大小是父View的剩余空間;如果父View是AT_MOST,那么View也是AT_MOST,并且大小是不會(huì)超過(guò)父容器的剩余空間。
- 當(dāng)View的寬高是wrap_content時(shí),View的模式總是AT_MOST,并且大小不超過(guò)父View的剩余空間。
measure過(guò)程
View的measure過(guò)程由其measure方法完成,該方法是一個(gè)final方法,其會(huì)調(diào)用onMeasure方法,我們看一下相關(guān)代碼:
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;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
getDefaultSize方法返回測(cè)量寬高,setMeasuredDimension方法會(huì)將其返回的測(cè)量寬高設(shè)置到系統(tǒng)。需要注意的是,自定義View的時(shí)候如果默認(rèn)系統(tǒng)設(shè)置測(cè)量值的方法,那么wrap_content的作用效果跟match_parent一樣,為什么?回看一下上面的總結(jié),當(dāng)view的布局寬高為wrap_content時(shí),則specMode為AT_MOST,則測(cè)量尺寸為specSize,就是父容器的剩余可用尺寸,即match_parent的效果。
對(duì)于ViewGroup而言,它除了需要完成自己的measure過(guò)程外,還要完成其子View的measure過(guò)程。而由于不同的ViewGroup具有不同的布局特性,這就導(dǎo)致它們的測(cè)量細(xì)節(jié)不同,所以ViewGroup沒(méi)有定義測(cè)量過(guò)程,而是由其具體子類來(lái)實(shí)現(xiàn)測(cè)量細(xì)節(jié),這里我們看一下LinearLayout的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
...
measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec,
total == 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));
}
}
從上面這段代碼可以看出,LinearLayout會(huì)遍歷每個(gè)子View并對(duì)每個(gè)子View執(zhí)行measureChildBeforeLayout方法,這個(gè)方法會(huì)調(diào)用子元素的measure方法,這樣子View就開(kāi)始依次進(jìn)入measure過(guò)程,并且通過(guò)mTotalLength這個(gè)變量來(lái)記錄LinearLayout在豎直方向上的高度,每測(cè)量一個(gè)子元素,mTotalLength就會(huì)增加,增加的部分主要是子View的高度以及子View在豎直方向的margin等,當(dāng)子元素測(cè)量完畢后,LinearLayout會(huì)測(cè)量自己的大小。
mTotalLength += mPaddingTop + mPaddingBottom;
int heigthSize = mTotalLength;
heigthSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heigthSize, heightMeasureSpec, 0);
heigthSize = heigthSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
對(duì)于vertical的LinearLayout而言,它在水平方向的測(cè)量過(guò)程遵循View的測(cè)量過(guò)程,在豎直方向上的測(cè)量過(guò)程則與View有所不同。具體來(lái)說(shuō),如果它的布局中高度采用的是match_parent或者具體數(shù)值,那么它的測(cè)量過(guò)程和View一致,即高度為specSize;如果采用的是wrap_content,那么它的高度是所有子View所占用的高度總和,但是仍然不能超過(guò)它的父容器的剩余空間,當(dāng)然它的最終高度還要考慮其在豎直方向的padding,這個(gè)過(guò)程可以進(jìn)一步看如下代碼:
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);
}
接下來(lái)我們看一下其measure子View的過(guò)程,其提供了一系列measureChild的方法,用來(lái)發(fā)起對(duì)子View的測(cè)量,將具體的measure過(guò)程委托給相關(guān)的子View:
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);
}
簡(jiǎn)單概括一下整個(gè)流程:measure過(guò)程始于DecorView,通過(guò)不斷遍歷子View的measure方法,根據(jù)ViewGroup的MeasureSpec以及子View的LayoutParams來(lái)決定子View的MeasureSpec,從而獲取子View的測(cè)量寬高,然后逐層返回。
需要注意的是:View的measure過(guò)程和Activity的生命周期方法不是同步執(zhí)行的,因此無(wú)法保證Activity執(zhí)行了onCreate、onStart、onResume時(shí)某個(gè)View已經(jīng)測(cè)量完畢了。如果View還沒(méi)有測(cè)量完畢,那么獲得的寬高就都是0。以下是幾種解決方法:
- Activity/View#onWindowFocusChanged():onWindowFocusChanged方法表示View已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,這個(gè)時(shí)候去獲取寬高是沒(méi)問(wèn)題的。這個(gè)方法會(huì)被調(diào)用多次,當(dāng)Activity繼續(xù)執(zhí)行或者暫停執(zhí)行的時(shí)候,這個(gè)方法都會(huì)被調(diào)用。
- View.post():通過(guò)post將一個(gè)runnable投遞到消息隊(duì)列的尾部,然后等待Looper調(diào)用此runnable的時(shí)候,View也已經(jīng)初始化好了。
- ViewTreeObserver:使用ViewTreeObserver的眾多回調(diào)方法可以完成這個(gè)功能,比如使用onGlobalLayoutListener接口,當(dāng)View樹(shù)的狀態(tài)發(fā)生改變或者View樹(shù)內(nèi)部的View的可見(jiàn)性發(fā)生改變時(shí),onGlobalLayout方法將被回調(diào)。伴隨著View樹(shù)的狀態(tài)改變,這個(gè)方法也會(huì)被多次調(diào)用。
layout過(guò)程
layout過(guò)程確定View在屏幕上的顯示位置:即設(shè)置其left、top、right和bottom,這幾個(gè)值構(gòu)成的矩形區(qū)域就是該View顯示的位置。一般由ViewGroup進(jìn)行,當(dāng)ViewGroup的位置確定以后,它在onLayout方法中會(huì)調(diào)用所有的子View的layout方法,遞歸進(jìn)行,從而完成整個(gè)layout過(guò)程。這里我們?nèi)匀灰訪inearLayout為例看一下其layout過(guò)程,LinearLayout有橫向和縱向布局,這里我們簡(jiǎn)要看一下縱向布局:
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);
}
}
...
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
final int width = right - left;
int childRight = width - mPaddingRight;
//子View可用的寬度
int childSpace = width - paddingLeft - mPaddingRight;
//子View的數(shù)量
final int count = getVirtualChildCount();
//majorGravity是LinearLayout的android:gravity屬性,即自己在父容器中的位置,垂直布局時(shí),自己的gravity只能是vertical
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
//minorGravity是LinearLayout中子View設(shè)置的gravity屬性,接下來(lái)layout子組件時(shí)會(huì)用到,垂直布局時(shí),子組件的gravity只能是horizontal
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//確定頂部坐標(biāo)
switch (majorGravity) {
//在父容器的底部,LinearLayout自己布局的頂部坐標(biāo)=我距離頂部的距離+父容器的高度-我自身的高度
case Gravity.BOTTOM:
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// 在父容器的中間,頂部起點(diǎn)=我距離頂部的距離+(父容器高度-我的高度)/ 2
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
//默認(rèn)或者設(shè)置在父容器的頂部,頂部起點(diǎn)=我設(shè)置的距離頂部的距離
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//上面的case也說(shuō)明,垂直布局時(shí),自己的位置屬性在頂部,底部,還有垂直居中下才有效
//遞歸布局子組件
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;
}
//根據(jù)上面三個(gè)case分析,在Linearlayout垂直布局時(shí),子組件的位置屬性只有水平居中,居左,居右三個(gè)有效。
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
//計(jì)算子View的頂部坐標(biāo)
childTop += lp.topMargin;
//設(shè)置子組件的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
//由于是縱向布局,因此子組件的頂部坐標(biāo)應(yīng)該是在上一個(gè)的基礎(chǔ)上累加
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
這個(gè)方法的整體過(guò)程可以分為三步:
- 根據(jù)容器的android:gravity屬性計(jì)算第一個(gè)子View的頂部起始坐標(biāo)(childTop);
- 遍歷子View,根據(jù)子View的android:layout_gravity屬性計(jì)算子View左邊的坐標(biāo);
- 然后計(jì)算出子View頂部的坐標(biāo)(vertical方向時(shí),縱坐標(biāo)是累加的,而橫坐標(biāo)每次都是從0開(kāi)始),最后調(diào)用setChildFrame方法對(duì)子View進(jìn)行l(wèi)ayout。
同時(shí)我們可以知道:對(duì)于LinearLayout而言:
- layout_gravity指的是自身在父容器的位置;gravity指的是子組件相對(duì)于自己的位置。當(dāng)自身的layout_gravity和父容器對(duì)自己默認(rèn)的gravity沖突時(shí),優(yōu)先選擇自身的layout_gravity。
- 當(dāng)android:orientation="horizontal"時(shí),自身的layout_gravity屬性有效取值為:right,left,center_horizontal;子組件的layout_gravity有效取值為:top,bottom,center_vertical。當(dāng)android:orientation="vertical"時(shí),自身的layout_gravity屬性有效取值為:top,bottom,center_vertical;子組件的layout_gravity有效取值為:left、right或者center_horizontal。
draw過(guò)程
draw過(guò)程決定了View的顯示,其流程比較簡(jiǎn)單,一般分為以下幾步:
- 繪制背景:background.draw(canvas);
- 繪制自己:onDraw();
- 繪制children:dispatchDraw;
- 繪制裝飾:onDrawScrollBars。
3.4.4 自定義View
自定義View注意事項(xiàng):
- 繼承View需要支持wrap_content,并且處理padding,而繼承特定的View例如TextView不需要考慮。
- View中如果有線程或者動(dòng)畫,需要在onDetachedFromWindow方法中及時(shí)停止。
- 處理好View的滑動(dòng)沖突情況。