Android觸摸事件的傳遞(一)--MotionEvent

了解更多,移步Android觸摸事件傳遞機(jī)制系列詳解

1. MotionEvent 簡(jiǎn)介

  • Android 將所有的輸入事件都放在了 MotionEvent
  • 隨著安卓的不斷發(fā)展壯大,MotionEvent 也開(kāi)始變得越來(lái)越復(fù)雜
    MotionEvent 大事記
    5527952-83c88b2878d49a93.png

以上僅僅是簡(jiǎn)要的說(shuō)明幾次比較大的變動(dòng)
MotionEvent 負(fù)責(zé)集中處理所有類型設(shè)備的輸入事件,但是由于某些設(shè)備使用的幾率較小本文會(huì)忽略講解,或者簡(jiǎn)要講解,例如:

  1. 軌跡球只出現(xiàn)在最早的設(shè)備上,現(xiàn)代的設(shè)備上已經(jīng)見(jiàn)不到了,本文不再敘述。
  2. 觸控筆和手指處理流程基本相同,不再多說(shuō)。
  3. 鼠標(biāo)在手機(jī)上使用概率也比較小,會(huì)在文末簡(jiǎn)要介紹。

1.1 事件類型

涉及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;
  1. ACTION_DOWN: 第一個(gè)手指按下時(shí)
  2. ACTION_MOVE:按住一點(diǎn)在屏幕上移動(dòng)
  3. ACTION_UP:最后一個(gè)手指抬起時(shí)
  4. ACTION_CANCEL:當(dāng)前的手勢(shì)被取消了,并且再也不會(huì)接收到后續(xù)的觸摸事件,這時(shí)我們就像ACTION_UP一樣對(duì)待他以結(jié)束該手勢(shì)操作,但是卻不執(zhí)行我們?cè)贏CTION_UP時(shí)需要執(zhí)行的動(dòng)作。
    ViewGroup分發(fā)事件的機(jī)制。一般來(lái)說(shuō),如果一個(gè)子視圖接收了父視圖分發(fā)給它的ACTION_DOWN事件,那么與ACTION_DOWN事件相關(guān)的事件流就都要分發(fā)給這個(gè)子視圖,但是如果父視圖希望攔截其中的一些事件,不再繼續(xù)轉(zhuǎn)發(fā)事件給這個(gè)子視圖的話,那么就需要給子視圖一個(gè)ACTION_CANCEL事件。這在后續(xù)文章中源碼分析部分也有體現(xiàn)。
  5. ACTION_OUTSIDE: 表示用戶觸碰超出了正常的UI邊界.

A movement has happened outside of the normal bounds of the UI element. This does not provide a full gesture, but only the initial location of the movement/touch.
一個(gè)觸摸事件已經(jīng)發(fā)生了UI元素的正常范圍之外。因此不再提供完整的手勢(shì),只提供 運(yùn)動(dòng)/觸摸 的初始位置。

我們知道,正常情況下,如果初始點(diǎn)擊位置在該視圖區(qū)域之外,該視圖根本不可能會(huì)收到事件,然而,萬(wàn)事萬(wàn)物都不是絕對(duì)的,肯定還有一些特殊情況,你可曾還記得點(diǎn)擊 Dialog 區(qū)域外關(guān)閉嗎?Dialog就是一個(gè)特殊的視圖(沒(méi)有占滿屏幕大小的窗口),能夠接收到視圖區(qū)域外的事件(雖然在通常情況下你根本用不到這個(gè)事件),除了 Dialog之外,你最可能看到這個(gè)事件的場(chǎng)景是懸浮窗,當(dāng)然啦,想要接收到視圖之外的事件需要一些特殊的設(shè)置。

設(shè)置視圖的 WindowManager 布局參數(shù)的 flags為FLAG_WATCH_OUTSIDE_TOUCH ,這樣點(diǎn)擊事件發(fā)生在這個(gè)視圖之外時(shí),該視圖就可以接收到一個(gè) ACTION_OUTSIDE
事件。

  1. ACTION_POINTER_DOWN:代表用戶又使用一個(gè)手指觸摸到屏幕上,也就是說(shuō),在已經(jīng)有一個(gè)觸摸點(diǎn)的情況下,又新出現(xiàn)了一個(gè)觸摸點(diǎn)。
  2. ACTION_POINTER_UP::代表用戶的一個(gè)手指離開(kāi)了觸摸屏,但是還有其他手指還在觸摸屏上。也就是說(shuō),在多個(gè)觸摸點(diǎn)存在的情況下,其中一個(gè)觸摸點(diǎn)消失了。它與ACTION_UP的區(qū)別就是,它是在多個(gè)觸摸點(diǎn)中的一個(gè)觸摸點(diǎn)消失時(shí)(此時(shí),還有觸摸點(diǎn)存在,也就是說(shuō)用戶還有手指觸摸屏幕)產(chǎn)生,而8. ACTION_UP可以說(shuō)是最后一個(gè)觸摸點(diǎn)消失時(shí)產(chǎn)生。會(huì)在多指觸摸和Pointers章節(jié)詳解。

1.2 坐標(biāo)

event.getX(); //觸摸點(diǎn)相對(duì)于View左上角為原點(diǎn)坐標(biāo)系的X坐標(biāo)
event.getY(); //觸摸點(diǎn)相對(duì)于View左上角為原點(diǎn)坐標(biāo)系的Y坐標(biāo)
event.getRawX(); //觸摸點(diǎn)相對(duì)于屏幕左上角為原點(diǎn)坐標(biāo)系的X坐標(biāo)
event.getRawY(); //觸摸點(diǎn)相對(duì)于屏幕左上角為原點(diǎn)坐標(biāo)系的Y坐標(biāo)

2 單點(diǎn)觸控

主要涉及以下幾個(gè)事件:

  • 當(dāng)我們?cè)诓僮魇謾C(jī)屏幕時(shí),哪怕只是輕輕點(diǎn)擊一下,系統(tǒng)也會(huì)產(chǎn)生一系列的觸摸事件(MotionEvent)對(duì)象。具體都會(huì)有哪些對(duì)象產(chǎn)生和我們的操作密不可分。
  • 這個(gè)動(dòng)作所產(chǎn)生的一系列事件,被稱為一個(gè)事件流,通常包括一個(gè)ACTION_DOWN事件,很多個(gè)ACTION_MOVE事件,和一個(gè)ACTION_UP事件。
    輕輕點(diǎn)擊一下
觸摸手勢(shì):0--ACTION_DOWN
觸摸手勢(shì):1--ACTION_UP

點(diǎn)擊按下后滑動(dòng)一下再抬起

com.zlq.customwidget V/TouchEventTest: 觸摸手勢(shì):0--ACTION_DOWN
com.zlq.customwidget V/TouchEventTest: 觸摸手勢(shì):2--ACTION_MOVE
com.zlq.customwidget V/TouchEventTest: 觸摸手勢(shì):2--ACTION_MOVE
...
com.zlq.customwidget V/TouchEventTest: 觸摸手勢(shì):1--ACTION_UP

3 多點(diǎn)觸控

  • Android 在 2.0 版本的時(shí)候開(kāi)始支持多點(diǎn)觸控,一旦出現(xiàn)了多點(diǎn)觸控,很多東西就突然之間變得麻煩起來(lái)了,首先要解決的問(wèn)題就是 多個(gè)手指同時(shí)按在屏幕上,會(huì)產(chǎn)生很多的事件,這些事件該如何區(qū)分呢?
  • 為了區(qū)分這些事件,工程師們用了一個(gè)很簡(jiǎn)單的辦法--編號(hào),當(dāng)手指第一次按下時(shí)產(chǎn)生一個(gè)唯一的號(hào)碼,手指抬起或者事件被攔截就回收編號(hào),就這么簡(jiǎn)單。
  • 第一次按下的手指特殊處理作為主指針,之后按下的手指作為輔助指針,然后隨之衍生出來(lái)了以下事件(注意增加的事件和事件簡(jiǎn)介的變化):


    5527952-2646c8c2d7d8c0ce.png

    和以下方法:


    5527952-224fe0e625a59daa.png

4 getAction()getActionMasked()

  • 當(dāng)多個(gè)手指在屏幕上按下的時(shí)候,會(huì)產(chǎn)生大量的事件,如何在獲取事件類型的同時(shí)區(qū)分這些事件就是一個(gè)大問(wèn)題了。
  • 一般來(lái)說(shuō)我們可以通過(guò)為事件添加一個(gè)int類型的index屬性來(lái)區(qū)分,但是我們知道谷歌工程師是有潔癖的(在 自定義View分類與流程 的onMeasure中已經(jīng)見(jiàn)識(shí)過(guò)了),為了添加一個(gè)通常數(shù)值不會(huì)超過(guò)10的index屬性就浪費(fèi)一個(gè)int大小的空間簡(jiǎn)直是不能忍受的,于是工程師們將這個(gè)index屬性和事件類型直接合并了。
  • int類型共32位(0x00000000),他們用最低8位(0x000000ff)表示事件類型,再往前的8位(0x0000ff00)表示事件編號(hào),以手指按下為例講解數(shù)值是如何合成的:

ACTION_DOWN 的默認(rèn)數(shù)值為 (0x00000000)
ACTION_POINTER_DOWN 的默認(rèn)數(shù)值為 (0x00000005)

5527952-1ae9b16b46f6c5d1.png

注意:

  • 上面表格中用粗體標(biāo)示出的數(shù)值,可以看到隨著按下手指數(shù)量的增加,這個(gè)數(shù)值也是一直變化的,進(jìn)而導(dǎo)致我們使用 getAction() 獲取到的數(shù)值無(wú)法與標(biāo)準(zhǔn)的事件類型進(jìn)行對(duì)比,為了解決這個(gè)問(wèn)題,他們創(chuàng)建了一個(gè) getActionMasked() 方法,這個(gè)方法可以清除index數(shù)值,讓其變成一個(gè)標(biāo)準(zhǔn)的事件類型。
  1. 多點(diǎn)觸控時(shí)必須使用 getActionMasked() 來(lái)獲取事件類型。
  2. 單點(diǎn)觸控時(shí)由于事件數(shù)值不變,使用 getAction()getActionMasked()兩個(gè)方法都可以。
  3. 使用 getActionIndex() 可以獲取到這個(gè)index數(shù)值。不過(guò)請(qǐng)注意,getActionIndex()只在downup 時(shí)有效,move時(shí)是無(wú)效的。
    目前來(lái)說(shuō)獲取事件類型使用 getActionMasked() 就行了,但是如果一定要編譯時(shí)兼容古董版本的話,可以考慮使用這樣的寫(xiě)法:
final int action = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
                ? event.getActionMasked()
                : event.getAction();
switch (action){
    case MotionEvent.ACTION_DOWN:
        // TODO
        break;
}

5 PointId

  • 雖然前面剛剛說(shuō)了一個(gè)actionIndex,可以使用getActionIndex()獲得,但通過(guò) actionIndex字面意思知道,這個(gè)只表示事件的序號(hào),而且根據(jù)其說(shuō)明文檔解釋,這個(gè) ActionIndex 只有在手指按下(down)和抬起(up)時(shí)是有用的,在移動(dòng)(move)時(shí)是沒(méi)有用的,事件追蹤非常重要的一環(huán)就是移動(dòng)(move),然而它卻沒(méi)卵用
  • PointId 在手指按下時(shí)產(chǎn)生,手指抬起或者事件被取消后消失,是一個(gè)事件流程中唯一不變的標(biāo)識(shí),可以在手指按下時(shí) 通過(guò) getPointerId(int pointerIndex) 獲得。 (參數(shù)中的 pointerIndex 就是 actionIndex)

6 歷史數(shù)據(jù)(批處理)

  • 由于我們的設(shè)備非常靈敏,手指稍微移動(dòng)一下就會(huì)產(chǎn)生一個(gè)移動(dòng)事件,所以移動(dòng)事件會(huì)產(chǎn)生的特別頻繁,為了提高效率,系統(tǒng)會(huì)將近期的多個(gè)移動(dòng)事件(move)按照事件發(fā)生的順序進(jìn)行排序打包放在同一個(gè) MotionEvent 中,與之對(duì)應(yīng)的產(chǎn)生了以下方法:
    5527952-0378815b75d3cc79.png

    注意:
    pin 全稱是 pointerIndex,表示第幾個(gè)手指,此處為了節(jié)省空間使用了縮寫(xiě)。
  1. 歷史數(shù)據(jù)只有 ACTION_MOVE 事件。
  2. 歷史數(shù)據(jù)單點(diǎn)觸控和多點(diǎn)觸控均可以用。
    下面是官方文檔給出的一個(gè)簡(jiǎn)單使用示例:
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));
     }
}

7 獲取事件發(fā)生的時(shí)間

5527952-ab6fb1455ee9ffb4.png
  1. pos 表示歷史數(shù)據(jù)中的第幾個(gè)數(shù)據(jù)。( pos < getHistorySize() )
  2. 返回值類型為 long,單位是毫秒。

獲取壓力(接觸面積大小)
MotionEvent支持獲取某些輸入設(shè)備(手指或觸控筆)的與屏幕的接觸面積和壓力大小,主要有以下方法:

5527952-ac693b084dc2a9e4.png

  1. pin全稱是pointerIndex,表示第幾個(gè)手指。(pin < getPointerCount() )
  2. pos表示歷史數(shù)據(jù)中的第幾個(gè)數(shù)據(jù)。( pos < getHistorySize() )
    注意:
  1. 獲取接觸面積大小和獲取壓力大小是需要硬件支持的。
  2. 非常不幸的是大部分設(shè)備所使用的電容屏不支持壓力檢測(cè),但能夠大致檢測(cè)出接觸面積。
  3. 大部分設(shè)備的 getPressure() 是使用接觸面積來(lái)模擬的。
  4. 由于某些未知的原因(可能系統(tǒng)版本和硬件問(wèn)題),某些設(shè)備不支持該方法。

我用不同的設(shè)備對(duì)這兩個(gè)方法進(jìn)行了測(cè)試,然而不同設(shè)備測(cè)試出來(lái)的結(jié)果不相同,之后經(jīng)過(guò)我多方查證,發(fā)現(xiàn)是系統(tǒng)問(wèn)題,有的設(shè)備上只有 getSize() 能用,有的設(shè)備上只有 getPressure() 能用,而有的則兩個(gè)都不能用。

由于獲取接觸面積和獲取壓力大小受系統(tǒng)和硬件影響,使用的時(shí)候一定要進(jìn)行數(shù)據(jù)檢測(cè),以防因?yàn)樵O(shè)備問(wèn)題而導(dǎo)致程序出錯(cuò)。

8 鼠標(biāo)事件

由于觸控筆事件和手指事件處理流程大致相同,所以就不講解了,這里講解一下與鼠標(biāo)相關(guān)的幾個(gè)事件:


5527952-da662a3bcd2fe073.png

1、這些事件類型是 安卓4.0 (API 14) 才添加的。2、使用 getActionMasked() 獲得這些事件類型。3、這些事件不會(huì)傳遞到 onTouchEvent(MotionEvent) 而是傳遞到 onGenericMotionEvent(MotionEvent) 。

9 輸入設(shè)備類型判斷

輸入設(shè)備類型判斷也是安卓4.0 (API 14) 才添加的,主要包括以下幾種設(shè)備:

5527952-983a57cf78336d1d.png

使用 getToolType(int pointerIndex) 來(lái)獲取對(duì)應(yīng)的輸入設(shè)備類型,pointIndex可以為0,但必須小于 getPointerCount()

參考

自定義View進(jìn)階《十四》——MotionEvent詳解
Android觸摸事件--MotionEvent

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容