了解更多,移步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)要講解,例如:
- 軌跡球只出現(xiàn)在最早的設(shè)備上,現(xiàn)代的設(shè)備上已經(jīng)見(jiàn)不到了,本文不再敘述。
- 觸控筆和手指處理流程基本相同,不再多說(shuō)。
- 鼠標(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;
-
ACTION_DOWN: 第一個(gè)手指按下時(shí) -
ACTION_MOVE:按住一點(diǎn)在屏幕上移動(dòng) -
ACTION_UP:最后一個(gè)手指抬起時(shí) -
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)。 -
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
事件。
-
ACTION_POINTER_DOWN:代表用戶又使用一個(gè)手指觸摸到屏幕上,也就是說(shuō),在已經(jīng)有一個(gè)觸摸點(diǎn)的情況下,又新出現(xiàn)了一個(gè)觸摸點(diǎn)。 -
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)

注意:
- 上面表格中用粗體標(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)的事件類型。
- 多點(diǎn)觸控時(shí)必須使用
getActionMasked()來(lái)獲取事件類型。 - 單點(diǎn)觸控時(shí)由于事件數(shù)值不變,使用
getAction()和getActionMasked()兩個(gè)方法都可以。 - 使用
getActionIndex() 可以獲取到這個(gè)index數(shù)值。不過(guò)請(qǐng)注意,getActionIndex()只在down和up時(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ě)。
- 歷史數(shù)據(jù)只有 ACTION_MOVE 事件。
- 歷史數(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í)間

- pos 表示歷史數(shù)據(jù)中的第幾個(gè)數(shù)據(jù)。( pos < getHistorySize() )
- 返回值類型為 long,單位是毫秒。
獲取壓力(接觸面積大小)
MotionEvent支持獲取某些輸入設(shè)備(手指或觸控筆)的與屏幕的接觸面積和壓力大小,主要有以下方法:

pin全稱是pointerIndex,表示第幾個(gè)手指。(pin < getPointerCount() )pos表示歷史數(shù)據(jù)中的第幾個(gè)數(shù)據(jù)。( pos < getHistorySize() )
注意:
- 獲取接觸面積大小和獲取壓力大小是需要硬件支持的。
- 非常不幸的是大部分設(shè)備所使用的電容屏不支持壓力檢測(cè),但能夠大致檢測(cè)出接觸面積。
- 大部分設(shè)備的 getPressure() 是使用接觸面積來(lái)模擬的。
- 由于某些未知的原因(可能系統(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è)事件:

1、這些事件類型是 安卓4.0 (API 14) 才添加的。2、使用 getActionMasked() 獲得這些事件類型。3、這些事件不會(huì)傳遞到 onTouchEvent(MotionEvent) 而是傳遞到 onGenericMotionEvent(MotionEvent) 。
9 輸入設(shè)備類型判斷
輸入設(shè)備類型判斷也是安卓4.0 (API 14) 才添加的,主要包括以下幾種設(shè)備:

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



