雖然在前面寫(xiě)自定義View的時(shí)候有提過(guò)事件的傳遞機(jī)制,但是并沒(méi)有全面系統(tǒng)的學(xué)習(xí)和記錄,趁著寫(xiě)這篇博客的機(jī)會(huì),把View的事件體系好好學(xué)習(xí)一遍,這篇博客里面不光有書(shū)中的內(nèi)容,也有我自己的見(jiàn)解。

一、View基礎(chǔ)知識(shí)
1. 什么是View
View是Android中所有控件的基類(lèi),不管是類(lèi)似于Button還是類(lèi)似于RelativeLayout,它們的共同基類(lèi)都是View,所以說(shuō),View是界面層的控件的一種抽象,它代表了一個(gè)控件,除了View,還有ViewGroup,ViewGroup可以翻譯成控件組,內(nèi)部包含了許多控件,即一組View。ViewGroup也繼承了View,這就意味著,View本身就可以是單個(gè)控件,也可以是多個(gè)控件組成的一組控件。
2.View的位置參數(shù)
View的位置主要是由它的四個(gè)頂點(diǎn)決定的,分別對(duì)應(yīng)于View的四個(gè)屬性:top、left、right、bottom(對(duì)應(yīng)的是左上右下兩個(gè)點(diǎn)的坐標(biāo))。需要注意的是,這些坐標(biāo)都是相對(duì)于View的父容器來(lái)說(shuō)的,因此它是一種相對(duì)坐標(biāo)。View的坐標(biāo)和父容器的關(guān)系如圖

我們很容易得出View的寬高和坐標(biāo)的關(guān)系:
width = right - left
height = bottom - top
如何獲得這四個(gè)參數(shù)呢,很簡(jiǎn)單:
left = getLeft(); right = getRight(); top = getTop(); bottom = getBottom();
從Android3.0開(kāi)始,View增加了額外的幾個(gè)參數(shù):x、y、translationX、translationY ,其中x和y 是View左上角的坐標(biāo),而translationX和translationY是View左上角相對(duì)于父容器的偏移量。這幾個(gè)參數(shù)也是相對(duì)于父容器的坐標(biāo),并且translationX和translationY的默認(rèn)值是0,和View的四個(gè)基本位置參數(shù)一樣,View也為它們提供了get/set方法,這幾個(gè)參數(shù)的換算關(guān)系如下:
x = left + translationX ;
y = top + translationY ;
View在平移的過(guò)程中,top和left表示的是原始左上角的位置信息,其值并不會(huì)發(fā)生改變,此時(shí)發(fā)送改變的是x,y、translationX和translationY這四個(gè)參數(shù)。
3. MotionEvent和TouchSlop
3.1 MotionEvent
在手指接觸屏幕后所產(chǎn)生的一系列事件中,典型的事件由如下幾種:
ACTION_DOWN : 手指剛接觸屏幕。
ACTION_MOVE: 手指在屏幕上移動(dòng)。
ACTION_UP :手指從屏幕上松開(kāi)的一瞬間。
正常情況下,一次手指觸摸屏幕的行為會(huì)觸發(fā)一系列的點(diǎn)擊事件,考慮如下幾種情況:
1.點(diǎn)擊屏幕后離開(kāi)松開(kāi),事件序列為:DOWN -> UP.
2.點(diǎn)擊屏幕滑動(dòng)一會(huì)兒再松開(kāi),事件序列為 :DOWN -> MOVE -> ... -> MOVE -> UP.
上述三種情況時(shí)典型的事件序列,同時(shí)通過(guò)MotionEvent對(duì)象我們可以得到點(diǎn)擊事件發(fā)生的x和y坐標(biāo)。為此,系統(tǒng)提供了兩組方法: getX/getY 和 getRawX/getRawY。它們的區(qū)別其實(shí)很簡(jiǎn)單:getX/getY返回的是相對(duì)于當(dāng)前View左上角的x和y坐標(biāo),而getRawX/getRawY返回的是相對(duì)于手機(jī)屏幕左上角的x和y坐標(biāo)。
3.2 TouchSlop
TouchSlop是系統(tǒng)所能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離,當(dāng)手指在屏幕上滑動(dòng)的時(shí)候,如果兩次滑動(dòng)之間的距離小于這個(gè)常量,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作。這是一個(gè)常量,和設(shè)備有關(guān),在不同設(shè)備上這個(gè)值可能是不同的,通過(guò)如下方式即可獲取這個(gè)常量:ViewConfiguration.get(getContext()).getScaledTouchSlop(); 這個(gè)值是8dp。當(dāng)我們?cè)谔幚砘瑒?dòng)時(shí),可以利用這個(gè)常量來(lái)進(jìn)行一些過(guò)濾。如果兩次滑動(dòng)的距離小于這個(gè)值,那么我們就認(rèn)為它們不是滑動(dòng)。
4. 速度追蹤、手勢(shì)檢測(cè)、Scroller
4.1 Velocity Tracker 速度追蹤
用于追蹤手指在滑動(dòng)過(guò)程中的速度,包括水平和豎直方向的速度。在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
接著我們就可以來(lái)獲取速度了,但是獲取速度之前必須先計(jì)算速度:
velocityTracker.computeCurrentVelocity(1000);//在1000ms中的速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
這里的速度是指一段時(shí)間內(nèi)手指滑過(guò)的像素?cái)?shù),比如時(shí)間間隔設(shè)為1000ms時(shí),在1s內(nèi),手指在水平方向從左向右滑過(guò)100像素,那么水平速度就是100。速度可能為負(fù)數(shù),當(dāng)手指從右向左滑動(dòng)時(shí),產(chǎn)生的速度就是負(fù)數(shù),如果時(shí)間間隔是100ms,100ms內(nèi)從左向右滑過(guò)100像素,那么速度就是100/0.1s = 1000像素。
速度 = (終點(diǎn)位置 - 起點(diǎn)位置)/ 時(shí)間段 ;
當(dāng)不使用它的時(shí)候,需要調(diào)用clear方法來(lái)重置并回收內(nèi)存:
velocityTracker.clear();
velocityTracker.recycle();
4.2 GestureDetector 手勢(shì)檢測(cè)
手勢(shì)檢測(cè),用于輔助檢測(cè)用戶(hù)的單擊,滑動(dòng),長(zhǎng)按,雙擊等行為。
GestureDetector的使用,首先需要?jiǎng)?chuàng)建一個(gè)GestureDetector 對(duì)象并繼承OnGestureListener和OnDoubleTapListener接口,并接管View的onTouchEvent方法,在待監(jiān)聽(tīng)的View的onTouchEvent方法中添加如下實(shí)現(xiàn):
boolean b = gestureDetector.onTouchEvent(event);
return b;
然后我們就可以有選擇的實(shí)現(xiàn)這兩個(gè)接口中的方法了:


在實(shí)際開(kāi)發(fā)中,如果只是監(jiān)聽(tīng)滑動(dòng)相關(guān)的,建議在onTouchEvent中實(shí)現(xiàn),如果是監(jiān)聽(tīng)雙擊這種行為,使用GestureDetector。。
4.3 Scroller
當(dāng)我們使用View的scrollTo/scrollBy方法來(lái)進(jìn)行滑動(dòng)時(shí),其過(guò)程是瞬間完成的,有了Scroller,我們就可以實(shí)現(xiàn)有過(guò)渡效果的滑動(dòng),其過(guò)程不是瞬間完成的,而是在一定的時(shí)間間隔內(nèi)完成的。使用Scroller進(jìn)行彈性滑動(dòng)的代碼是固定寫(xiě)法的。
Scroller scroller = new Scroller(getContext());
private void smoothScrollerTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
scroller.startScroll(scrollX,0,delta,1000);
invalidate();//重繪界面
}
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
二、View的滑動(dòng)
在Android設(shè)備上,滑動(dòng)幾乎是應(yīng)用的標(biāo)配,通過(guò)三種方法可以實(shí)現(xiàn)View的滑動(dòng):第一種通過(guò)View本身提供的ScrollTo/ScrollBy方法來(lái)實(shí)現(xiàn)滑動(dòng);第二種通過(guò)動(dòng)畫(huà)給View施加平移效果來(lái)實(shí)現(xiàn)滑動(dòng);第三種通過(guò)改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動(dòng)。
1 使用ScrollTo/ScrollBy
為了實(shí)現(xiàn)View的滑動(dòng),View提供了專(zhuān)門(mén)的方法來(lái)實(shí)現(xiàn)這個(gè)功能,那就是ScrollTo和ScrollBy,這兩個(gè)方法的源碼比較簡(jiǎn)單:
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
從源碼中可以看出,scrollBy實(shí)際上也是調(diào)用了scrollTo方法,它實(shí)現(xiàn)了基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo則實(shí)現(xiàn)了基于所傳遞參數(shù)的絕對(duì)滑動(dòng)。(scrollTo和scrollBy的區(qū)別:scrollTo是滾動(dòng)到。滾動(dòng)到10像素,-30像素,scrollBy,在原來(lái)的基礎(chǔ)上滾動(dòng),假如上一次滾動(dòng)到10像素,如果此時(shí)使用scrollBy(30,0)就是向右滾動(dòng)到40像素處;而假如此時(shí)使用scrollBy(-20,0)就是滾動(dòng)到-10像素的位置,即向左滾動(dòng)到-10像素處)
我們要明白滑動(dòng)過(guò)程中View內(nèi)部的兩個(gè)屬性mScrollX和mScrollY的改變規(guī)則,這兩個(gè)屬性可以通過(guò)getScrollX和getScrollY方法得到。在滑動(dòng)過(guò)程中,mScrollX的值總是等于View的左邊緣和View內(nèi)容左邊緣在水平方向的距離,而mScrollY的值總是等于View上邊緣和View內(nèi)容上邊緣在豎直方向的距離。View邊緣指View的位置,由4個(gè)頂點(diǎn)組成,而View內(nèi)容邊緣是指View中內(nèi)容的邊緣,scrollTo和scrollBy只能改變
四、View的事件分發(fā)機(jī)制
4.1 點(diǎn)擊事件的傳遞規(guī)則
點(diǎn)擊事件,即MotionEvent,所謂點(diǎn)擊事件的事件分發(fā),就是當(dāng)一個(gè)MotionEvent產(chǎn)生了以后,系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的View,而這個(gè)傳遞的過(guò)程就是分發(fā)的過(guò)程,點(diǎn)擊事件的分發(fā)過(guò)程有三個(gè)很重要的方法來(lái)完成,dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event)
dispatchTouchEvent:用來(lái)進(jìn)行事件的分發(fā),如果事件能夠傳遞給當(dāng)前View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev)
onInterceptTouchEvent: 在dispatchTouchEvent內(nèi)部調(diào)用,用來(lái)判斷是否攔截某個(gè)事件,如果當(dāng)前View攔截了某個(gè)事件,那么在同一方法序列中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件
/**
* Implement this method to handle touch screen motion events.
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event)
onTouchEvent:在dispatchTouchEvent中調(diào)用,用來(lái)處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗,則在同一事件序列中,當(dāng)前View無(wú)法再次接收到事件。
這三個(gè)方法的關(guān)系我們先用一段偽代碼表示一下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptHoverEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
對(duì)于一個(gè)根ViewGroup來(lái)說(shuō),點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的dispatchTouchEvent就會(huì)被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè)ViewGroup處理,即它的onTouchEvent方法就會(huì)被調(diào)用;如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當(dāng)前事件,這時(shí)當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。
當(dāng)一個(gè)View需要處理事件時(shí),如果它設(shè)置了OnTouchListener,那么onTouchListener中的onTouch方法會(huì)被回調(diào)。這時(shí)事件如何處理還要看onTouch的返回值,如果返回false,則當(dāng)前View的onTouchEvent方法會(huì)被調(diào)用;如果返回true,那么onTouchEvent方法將不會(huì)被調(diào)用。由此可見(jiàn),給View設(shè)置的OnTouchListener,其優(yōu)先級(jí)比onTouchEvent要高。
當(dāng)一個(gè)點(diǎn)擊事件產(chǎn)生后,它的傳遞過(guò)程遵循如下順序:Activity --> Window --> View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最后Window再傳遞給頂級(jí)View,頂級(jí)View接收到事件后,就會(huì)按照事件分發(fā)機(jī)制去分發(fā)事件??紤]一種情況,如果一個(gè)View的onTouchEvent返回false,那么它的父容器的onTouchEvent將會(huì)被調(diào)用,以此類(lèi)推。如果所有的元素都不處理這個(gè)事件,那么這個(gè)事件將會(huì)最終傳遞給Activity處理,即Activity的onTouchEvent方法會(huì)被調(diào)用。