Android觸摸事件--MotionEvent

開篇

最近在研究自定義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坐標
觸摸坐標信息.png
 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: 第一個手指按下時
  1. ACTION_MOVE:按住一點在屏幕上移動
  2. ACTION_UP:最后一個手指抬起時
  3. ACTION_CANCEL:當前的手勢被取消了,并且再也不會接收到后續的觸摸事件,這時我們就像ACTION_UP一樣對待他以結束該手勢操作,但是卻不執行我們在ACTION_UP時需要執行的動作。
    要理解這個類型,就必須要了解ViewGroup分發事件的機制。一般來說,如果一個子視圖接收了父視圖分發給它的ACTION_DOWN事件,那么與ACTION_DOWN事件相關的事件流就都要分發給這個子視圖,但是如果父視圖希望攔截其中的一些事件,不再繼續轉發事件給這個子視圖的話,那么就需要給子視圖一個ACTION_CANCEL事件。這在后續文章中源碼分析部分也有體現。
  4. ACTION_OUTSIDE: 表示用戶觸碰超出了正常的UI邊界.
  5. ACTION_POINTER_DOWN:代表用戶又使用一個手指觸摸到屏幕上,也就是說,在已經有一個觸摸點的情況下,又新出現了一個觸摸點。
  6. 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所代表的觸摸點相關事件坐標值。

那么,用戶先兩個手指先后接觸屏幕,同時滑動,然后在先后離開這一套動作所產生的事件流是什么樣的呢?
?它所產生的事件流如下:

  1. 先產生一個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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容