開篇
最近在研究自定義View方面的知識。而自定義View中很重要的一塊就是View的交互。這就牽涉到本系列文章要講到的Android 觸摸事件相關的知識。
而手指每次與屏幕的交互都會由gesture產生一系列的事件:由MotionEvent對象來表述。
MotionEvent 包含的信息
MotionEvent是用于描述一個運動事件(鼠標、筆、手指、軌跡球)的類。MotionEvent可能持有絕對或者相對運動和其他數據,這取決于設備的類型。
1、坐標
如開篇所述,每個觸摸事件都代表用戶在屏幕上的一個動作,而每個動作必定有其發生的位置。
在MotionEvent中有兩組可以獲得觸摸位置的函數
event.getX(); //觸摸點相對于View左上角為原點坐標系的X坐標
event.getY(); //觸摸點相對于View左上角為原點坐標系的Y坐標
event.getRawX(); //觸摸點相對于屏幕左上角為原點坐標系的X坐標
event.getRawY(); //觸摸點相對于屏幕左上角為原點坐標系的Y坐標
final float offsetX = mScrollX - child.mLeft;//子View相對于父View坐標系的偏移值-X
final float offsetY = mScrollY - child.mTop;//子View相對于父View坐標系的偏移值-Y
event.offsetLocation(offsetX, offsetY);//保證傳遞給子View時,event的getX()、getY() 是相對于該子View的坐標系的坐標值。
handled = child.dispatchTouchEvent(event);//子View分發事件
event.offsetLocation(-offsetX, -offsetY);
這段代碼清晰展示了父視圖把事件分發給子視圖時,getX()和getY所獲得的相關坐標是如何改變的。當父視圖處理事件時,上述兩個函數獲得的相對坐標是相對于父視圖的,當需要將該事件分發給子視圖時,就通過上邊這段代碼,調整了相對坐標的值,讓其變為相對于子視圖。
2、事件類型
action code能引起View狀態的變化,比如手指向上滑動、向下滑動。
設計MotionEvent的事件類型主要有:
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
public static final int ACTION_CANCEL = 3;
public static final int ACTION_OUTSIDE = 4;
public static final int ACTION_POINTER_DOWN = 5;
public static final int ACTION_POINTER_UP = 6;
-
ACTION_DOWN
: 第一個手指按下時
-
ACTION_MOVE
:按住一點在屏幕上移動 -
ACTION_UP
:最后一個手指抬起時 -
ACTION_CANCEL
:當前的手勢被取消了,并且再也不會接收到后續的觸摸事件,這時我們就像ACTION_UP
一樣對待他以結束該手勢操作,但是卻不執行我們在ACTION_UP
時需要執行的動作。
要理解這個類型,就必須要了解ViewGroup分發事件的機制。一般來說,如果一個子視圖接收了父視圖分發給它的ACTION_DOWN
事件,那么與ACTION_DOWN
事件相關的事件流就都要分發給這個子視圖,但是如果父視圖希望攔截其中的一些事件,不再繼續轉發事件給這個子視圖的話,那么就需要給子視圖一個ACTION_CANCEL
事件。這在后續文章中源碼分析部分也有體現。 -
ACTION_OUTSIDE
: 表示用戶觸碰超出了正常的UI邊界. -
ACTION_POINTER_DOWN
:代表用戶又使用一個手指觸摸到屏幕上,也就是說,在已經有一個觸摸點的情況下,又新出現了一個觸摸點。 -
ACTION_POINTER_UP
::代表用戶的一個手指離開了觸摸屏,但是還有其他手指還在觸摸屏上。也就是說,在多個觸摸點存在的情況下,其中一個觸摸點消失了。它與ACTION_UP的區別就是,它是在多個觸摸點中的一個觸摸點消失時(此時,還有觸摸點存在,也就是說用戶還有手指觸摸屏幕)產生,而ACTION_UP可以說是最后一個觸摸點消失時產生。會在多指觸摸和Pointers章節詳解。
3、其他屬性
getEdgeFlags():當事件類型是ActionDown時可以通過此方法獲得,手指觸控開始的邊界. 如果是的話,有如下幾種值:
public static final int EDGE_TOP = 0x00000001;
public static final int EDGE_BOTTOM = 0x00000002;
public static final int EDGE_LEFT = 0x00000004;
public static final int EDGE_RIGHT = 0x00000008;
EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
單點手勢操作
當我們在操作手機屏幕時,哪怕只是輕輕點擊一下,系統也會產生一系列的觸摸事件(MotionEvent)對象。具體都會有哪些對象產生和我們的操作密不可分。這個動作所產生的一系列事件,被稱為一個事件流,通常包括一個ACTION_DOWN事件,很多個ACTION_MOVE事件,和一個ACTION_UP事件。
輕輕點擊一下:
com.zlq.customwidget V/TouchEventTest: 觸摸手勢:0--ACTION_DOWN
com.zlq.customwidget V/TouchEventTest: 觸摸手勢:1--ACTION_UP
點擊按下后滑動一下再抬起:
com.zlq.customwidget V/TouchEventTest: 觸摸手勢:0--ACTION_DOWN
com.zlq.customwidget V/TouchEventTest: 觸摸手勢:2--ACTION_MOVE
com.zlq.customwidget V/TouchEventTest: 觸摸手勢:2--ACTION_MOVE
...
com.zlq.customwidget V/TouchEventTest: 觸摸手勢:1--ACTION_UP
我們在使用手勢判斷時一般如下:
int action = MotionEventCompat.getAction(event);
switch(action) {
case MotionEvent.ACTION_DOWN:
//按下時
Log.v("TouchEventTest","觸摸手勢:"+action+"--"+"ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
//移動時
Log.v("TouchEventTest","觸摸手勢:"+action+"--"+"ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
//離開時
Log.v("TouchEventTest","觸摸手勢:"+action+"--"+"ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
//取消時
Log.v("TouchEventTest","觸摸手勢:"+action+"--"+"ACTION_CANCEL");
break;
多指手勢操作和Pointers
為了可以表示多點觸摸的的動作,MotionEvent中引入了Pointer的概念,多點觸摸時每個手指產生一個運動軌跡。產生運動軌跡的各個手指或其他物體被稱為pointers。
- 一個MotionEvent對象中可能會存儲多個pointer的相關信息
- 每個pointer都有自己的事件類型,也有自己的橫軸坐標值。
- 每個pointer都會有一個自己的id和index。
- pointer的id在整個事件流中是不會發生變化的,但是index會發生變化。所以,當我們要記錄一個觸摸點的事件流時,就只需要保存其id,然后使用findPointerIndex(int)來獲得其index值,然后再獲得其他信息。
- MotionEvent類中的很多方法都是可以傳入一個int值作為參數的,其實傳入的就是pointer的index值。比如getX(pointerIndex)和getY(pointerIndex),此時,它們返回的就是index所代表的觸摸點相關事件坐標值。
那么,用戶先兩個手指先后接觸屏幕,同時滑動,然后在先后離開這一套動作所產生的事件流是什么樣的呢?
?它所產生的事件流如下:
- 先產生一個ACTION_DOWN事件,代表用戶的第一個手指接觸到了屏幕。
- 再產生一個 ACTION_POINTER_DOWN 事件,代表用戶的第二個手指接觸到了屏幕。
- 很多的 ACTION_MOVE 事件,但是在這些MotionEvent對象中,都保存著兩個觸摸點滑動的信息,相關的代碼我們會在文章的最后進行演示。
- 一個 ACTION_POINTER_UP 事件,代表用戶的一個手指離開了屏幕。
- 如果用戶剩下的手指還在滑動時,就會產生很多ACTION_MOVE事件。
- 一個 ACTION_UP 事件,代表用戶的最后一個手指離開了屏幕
跟單點手勢操作一樣,多點手勢操作的信息也是包含在方法onTouchEvent的MotionEvent參數內,MotionEvent中有如下函數可以獲取多點觸摸信息:
int getPointerCount() //手勢操作所包含的點的個數
int findPointerIndex(int pointerId) //根據pointerId找到pointer在MotionEvent中的index
int getPointerId(int pointerIndex) //根據MotionEvent中的index返回pointer的唯一標識
float getX(int pointerIndex) //返回手勢操作點的x坐標
float getY(int pointerIndex) //返回手勢操作點的y坐標
final int getActionMasked () //獲取特殊點的action
final int getActionIndex()// 用來獲取當前按下/抬起的點的標識。如果當前沒有任何點抬起/按下,該函數返回0。比如事件類型為ACTION_MOVE時,該值始終為0。
獲取事件類型 getAction 和 getActionMasked
從上一節我們可以得知,一個MotionEvent對象中可以包含多個觸摸點的事件。當MotionEvent對象只包含一個觸摸點的事件時,上邊兩個函數的結果是相同的,但是當包含多個觸摸點時,二者的結果就不同了。
?getAction獲得的int值是由pointer的index值和事件類型值組合而成的,而getActionWithMasked則只返回事件的類型值。
getAction返回結果中不同位所代表的含義:前8位代表id,后8位代表事件類型。
我們看下面解析&例子:
MotionEvent類中定義了兩個ACTION的掩碼:
public static final int ACTION_MASK = 0xff;
public static final int ACTION_POINTER_INDEX_MASK = 0xff00;
轉化為二進制就是:
ACTION_MASK = 0000000011111111
ACTION_POINTER_INDEX_MASK = 1111111100000000
假設我們操作時getAction()返回0x0105
,轉化為二進制就是
getAction() = 0000000100000101
這時,
int indexOriginal = getAction() & ACTION_POINTER_INDEX_MASK = 0000000100000000;
int index=indexOriginal >> 8 ;//就得到了pointer的index:00000001,即1.
int action= getAction() & ACTION_MASK =0000000000000101 ;//就得到了pointer的真正action,即5,即ACTION_POINTER_DOWN。
而getActionMasked()就會直接返回5
,和以上getAction() & ACTION_MASK運算后結果一樣。
以上例子中的推斷從MotionEvent的源碼中也可以得到佐證:
public static final int ACTION_MASK = 0xff;
public static final int ACTION_POINTER_INDEX_MASK = 0xff00;
public static final int ACTION_POINTER_INDEX_SHIFT = 8;
public final int getAction() {
return nativeGetAction(mNativePtr);
}
public final int getActionMasked() {
return nativeGetAction(mNativePtr) & ACTION_MASK;
}
public final int getActionIndex() {
return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
>> ACTION_POINTER_INDEX_SHIFT;
}
對于ACTION_DOWN、ACTION_UP之間的其他點(包括ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_POINTER_UP),Android稱之為maskedAction,可以使用函數public final int getActionMasked()來查詢這個動作是ACTION_POINTER_DOWN、ACTION_POINTER_UP還是ACTION_MOVE。
批處理
為了效率,Android系統在處理ACTION_MOVE事件時會將連續的幾個多觸點移動事件打包到一個MotionEvent對象中。我們可以通過getX(int)和getY(int)來獲得最近發生的一個觸摸點事件的坐標,然后使用getHistorical(int,int)和getHistorical(int,int)來獲得時間稍早的觸點事件的坐標,二者是發生時間先后的關系。所以,我們應該先處理通過getHistoricalXX相關函數獲得的事件信息,然后在處理當前的事件信息。
?下邊就是Android Guide中相關的例子:
void printSamples(MotionEvent ev) {
final int historySize = ev.getHistorySize();
final int pointerCount = ev.getPointerCount();
for (int h = 0; h < historySize; h++) {
System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)",
ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
}
}
System.out.printf("At time %d:", ev.getEventTime());
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)",
ev.getPointerId(p), ev.getX(p), ev.getY(p));
}
}
概覽
MotionEvent描述交互操作中的action code和坐標值等信息。坐標值信息包含該位置和其他運動信息。例如,當用戶第一次觸摸屏,該系統提供了一個觸摸事件到相應的視圖與動作代碼ACTION_DOWN 和一組軸值,包括X和觸摸和左右的壓力,尺寸信息的Y坐標和接觸區域的方向。
某些設備支持多點觸摸。多點觸摸時每個手指產生一個運動軌跡。產生運動軌跡的各個手指或其他物體被稱為pointers.MotionEvent 包含所有pointers的信息,就算其中有些已經不再移動了。
pointers的數量一般只會被個別手指的抬起放下所影響,特殊情況是當手勢取消時。
參考文獻
https://developer.android.com/reference/android/view/MotionEvent.html
http://www.lxweimin.com/p/0c863bbde8eb
http://my.oschina.net/banxi/blog/56421