雖然在前面寫自定義View的時候有提過事件的傳遞機制,但是并沒有全面系統的學習和記錄巍佑,趁著寫這篇博客的機會茴迁,把View的事件體系好好學習一遍,這篇博客里面不光有書中的內容萤衰,也有我自己的見解堕义。
一、View基礎知識
1. 什么是View
View是Android中所有控件的基類脆栋,不管是類似于Button還是類似于RelativeLayout倦卖,它們的共同基類都是View,所以說椿争,View是界面層的控件的一種抽象怕膛,它代表了一個控件,除了View秦踪,還有ViewGroup,ViewGroup可以翻譯成控件組褐捻,內部包含了許多控件,即一組View椅邓。ViewGroup也繼承了View柠逞,這就意味著,View本身就可以是單個控件景馁,也可以是多個控件組成的一組控件板壮。
2.View的位置參數
View的位置主要是由它的四個頂點決定的,分別對應于View的四個屬性:top合住、left绰精、right、bottom(對應的是左上右下兩個點的坐標)聊疲。需要注意的是茬底,這些坐標都是相對于View的父容器來說的,因此它是一種相對坐標获洲。View的坐標和父容器的關系如圖
我們很容易得出View的寬高和坐標的關系:
width = right - left
height = bottom - top
如何獲得這四個參數呢阱表,很簡單:
left = getLeft(); right = getRight(); top = getTop(); bottom = getBottom();
從Android3.0開始贡珊,View增加了額外的幾個參數:x最爬、y、translationX门岔、translationY 爱致,其中x和y 是View左上角的坐標,而translationX和translationY是View左上角相對于父容器的偏移量寒随。這幾個參數也是相對于父容器的坐標糠悯,并且translationX和translationY的默認值是0帮坚,和View的四個基本位置參數一樣,View也為它們提供了get/set方法互艾,這幾個參數的換算關系如下:
x = left + translationX ;
y = top + translationY ;
View在平移的過程中试和,top和left表示的是原始左上角的位置信息,其值并不會發(fā)生改變纫普,此時發(fā)送改變的是x阅悍,y、translationX和translationY這四個參數昨稼。
3. MotionEvent和TouchSlop
3.1 MotionEvent
在手指接觸屏幕后所產生的一系列事件中节视,典型的事件由如下幾種:
ACTION_DOWN : 手指剛接觸屏幕。
ACTION_MOVE: 手指在屏幕上移動假栓。
ACTION_UP :手指從屏幕上松開的一瞬間寻行。
正常情況下,一次手指觸摸屏幕的行為會觸發(fā)一系列的點擊事件但指,考慮如下幾種情況:
1.點擊屏幕后離開松開寡痰,事件序列為:DOWN -> UP.
2.點擊屏幕滑動一會兒再松開,事件序列為 :DOWN -> MOVE -> ... -> MOVE -> UP.
上述三種情況時典型的事件序列棋凳,同時通過MotionEvent對象我們可以得到點擊事件發(fā)生的x和y坐標拦坠。為此,系統提供了兩組方法: getX/getY 和 getRawX/getRawY剩岳。它們的區(qū)別其實很簡單:getX/getY返回的是相對于當前View左上角的x和y坐標贞滨,而getRawX/getRawY返回的是相對于手機屏幕左上角的x和y坐標。
3.2 TouchSlop
TouchSlop是系統所能識別出的被認為是滑動的最小距離拍棕,當手指在屏幕上滑動的時候晓铆,如果兩次滑動之間的距離小于這個常量,那么系統就不認為你是在進行滑動操作绰播。這是一個常量骄噪,和設備有關,在不同設備上這個值可能是不同的蠢箩,通過如下方式即可獲取這個常量:ViewConfiguration.get(getContext()).getScaledTouchSlop(); 這個值是8dp链蕊。當我們在處理滑動時,可以利用這個常量來進行一些過濾谬泌。如果兩次滑動的距離小于這個值滔韵,那么我們就認為它們不是滑動。
4. 速度追蹤掌实、手勢檢測陪蜻、Scroller
4.1 Velocity Tracker 速度追蹤
用于追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度贱鼻。在View的onTouchEvent方法中追蹤當前單擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
接著我們就可以來獲取速度了宴卖,但是獲取速度之前必須先計算速度:
velocityTracker.computeCurrentVelocity(1000);//在1000ms中的速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
這里的速度是指一段時間內手指滑過的像素數滋将,比如時間間隔設為1000ms時,在1s內症昏,手指在水平方向從左向右滑過100像素耕渴,那么水平速度就是100。速度可能為負數齿兔,當手指從右向左滑動時,產生的速度就是負數础米,如果時間間隔是100ms分苇,100ms內從左向右滑過100像素,那么速度就是100/0.1s = 1000像素屁桑。
速度 = (終點位置 - 起點位置)/ 時間段 医寿;
當不使用它的時候,需要調用clear方法來重置并回收內存:
velocityTracker.clear();
velocityTracker.recycle();
4.2 GestureDetector 手勢檢測
手勢檢測蘑斧,用于輔助檢測用戶的單擊靖秩,滑動,長按竖瘾,雙擊等行為沟突。
GestureDetector的使用,首先需要創(chuàng)建一個GestureDetector 對象并繼承OnGestureListener和OnDoubleTapListener接口捕传,并接管View的onTouchEvent方法惠拭,在待監(jiān)聽的View的onTouchEvent方法中添加如下實現:
boolean b = gestureDetector.onTouchEvent(event);
return b;
然后我們就可以有選擇的實現這兩個接口中的方法了:
在實際開發(fā)中,如果只是監(jiān)聽滑動相關的庸论,建議在onTouchEvent中實現职辅,如果是監(jiān)聽雙擊這種行為,使用GestureDetector聂示。域携。
4.3 Scroller
當我們使用View的scrollTo/scrollBy方法來進行滑動時,其過程是瞬間完成的鱼喉,有了Scroller秀鞭,我們就可以實現有過渡效果的滑動,其過程不是瞬間完成的蒲凶,而是在一定的時間間隔內完成的气筋。使用Scroller進行彈性滑動的代碼是固定寫法的。
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的滑動
在Android設備上宠默,滑動幾乎是應用的標配,通過三種方法可以實現View的滑動:第一種通過View本身提供的ScrollTo/ScrollBy方法來實現滑動灵巧;第二種通過動畫給View施加平移效果來實現滑動搀矫;第三種通過改變View的LayoutParams使得View重新布局從而實現滑動抹沪。
1 使用ScrollTo/ScrollBy
為了實現View的滑動,View提供了專門的方法來實現這個功能瓤球,那就是ScrollTo和ScrollBy融欧,這兩個方法的源碼比較簡單:
/**
* 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實際上也是調用了scrollTo方法卦羡,它實現了基于當前位置的相對滑動噪馏,而scrollTo則實現了基于所傳遞參數的絕對滑動。(scrollTo和scrollBy的區(qū)別:scrollTo是滾動到绿饵。滾動到10像素欠肾,-30像素,scrollBy拟赊,在原來的基礎上滾動刺桃,假如上一次滾動到10像素,如果此時使用scrollBy(30,0)就是向右滾動到40像素處吸祟;而假如此時使用scrollBy(-20瑟慈,0)就是滾動到-10像素的位置,即向左滾動到-10像素處)
我們要明白滑動過程中View內部的兩個屬性mScrollX和mScrollY的改變規(guī)則屋匕,這兩個屬性可以通過getScrollX和getScrollY方法得到葛碧。在滑動過程中,mScrollX的值總是等于View的左邊緣和View內容左邊緣在水平方向的距離过吻,而mScrollY的值總是等于View上邊緣和View內容上邊緣在豎直方向的距離吹埠。View邊緣指View的位置,由4個頂點組成疮装,而View內容邊緣是指View中內容的邊緣缘琅,scrollTo和scrollBy只能改變
四、View的事件分發(fā)機制
4.1 點擊事件的傳遞規(guī)則
點擊事件廓推,即MotionEvent刷袍,所謂點擊事件的事件分發(fā),就是當一個MotionEvent產生了以后樊展,系統需要把這個事件傳遞給一個具體的View呻纹,而這個傳遞的過程就是分發(fā)的過程,點擊事件的分發(fā)過程有三個很重要的方法來完成专缠,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:用來進行事件的分發(fā)涝婉,如果事件能夠傳遞給當前View哥力,那么此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件吩跋。
/**
* 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內部調用寞射,用來判斷是否攔截某個事件,如果當前View攔截了某個事件锌钮,那么在同一方法序列中桥温,此方法不會被再次調用,返回結果表示是否攔截當前事件
/**
* 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中調用梁丘,用來處理點擊事件侵浸,返回結果表示是否消耗當前事件,如果不消耗氛谜,則在同一事件序列中通惫,當前View無法再次接收到事件。
這三個方法的關系我們先用一段偽代碼表示一下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptHoverEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
對于一個根ViewGroup來說混蔼,點擊事件產生后,首先會傳遞給它珊燎,這時它的dispatchTouchEvent就會被調用惭嚣,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理悔政,即它的onTouchEvent方法就會被調用晚吞;如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當前事件谋国,這時當前事件就會繼續(xù)傳遞給它的子元素槽地,接著子元素的dispatchTouchEvent方法就會被調用,如此反復直到事件被最終處理芦瘾。
當一個View需要處理事件時捌蚊,如果它設置了OnTouchListener,那么onTouchListener中的onTouch方法會被回調近弟。這時事件如何處理還要看onTouch的返回值缅糟,如果返回false,則當前View的onTouchEvent方法會被調用祷愉;如果返回true窗宦,那么onTouchEvent方法將不會被調用。由此可見二鳄,給View設置的OnTouchListener赴涵,其優(yōu)先級比onTouchEvent要高。
當一個點擊事件產生后订讼,它的傳遞過程遵循如下順序:Activity --> Window --> View髓窜,即事件總是先傳遞給Activity,Activity再傳遞給Window欺殿,最后Window再傳遞給頂級View纱烘,頂級View接收到事件后杨拐,就會按照事件分發(fā)機制去分發(fā)事件±奚叮考慮一種情況哄陶,如果一個View的onTouchEvent返回false,那么它的父容器的onTouchEvent將會被調用哺壶,以此類推屋吨。如果所有的元素都不處理這個事件,那么這個事件將會最終傳遞給Activity處理山宾,即Activity的onTouchEvent方法會被調用至扰。