Android MotionEvent詳解

?在前邊幾篇博文中(《圖解Android事件傳遞之ViewGroup篇》《圖解Android事件傳遞之View篇》),我們已經(jīng)了解了android觸摸事件傳遞機(jī)制,接著我們?cè)賮硌芯恳幌屡c觸摸事件傳遞相關(guān)的幾個(gè)比較重要的類,比如MotionEvent。我們今天就來詳細(xì)說明一下這個(gè)類的各方面用法。

事件坐標(biāo)的含義

我們都知道,每個(gè)觸摸事件都代表用戶在屏幕上的一個(gè)動(dòng)作,而每個(gè)動(dòng)作必定有其發(fā)生的位置。在MotionEvent中就有一系列與標(biāo)觸摸事件發(fā)生位置相關(guān)的函數(shù):

  • getX()getY():由這兩個(gè)函數(shù)獲得的x,y值是相對(duì)的坐標(biāo)值,相對(duì)于消費(fèi)這個(gè)事件的視圖的左上點(diǎn)的坐標(biāo)。
  • getRawX()getRawY():有這兩個(gè)函數(shù)獲得的x,y值是絕對(duì)坐標(biāo),是相對(duì)于屏幕的。
    ?在之前的文章中,我們?cè)?jīng)分析過事件如何通過層層分發(fā),最終到達(dá)消費(fèi)它的視圖手中。其中ViewGroupdispatchTransformedTouchEvent函數(shù)有如下一段代碼:
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    event.offsetLocation(offsetX, offsetY);
    handled = child.dispatchTouchEvent(event);
    event.offsetLocation(-offsetX, -offsetY);

這段代碼清晰展示了父視圖把事件分發(fā)給子視圖時(shí),getX()getY所獲得的相關(guān)坐標(biāo)是如何改變的。當(dāng)父視圖處理事件時(shí),上述兩個(gè)函數(shù)獲得的相對(duì)坐標(biāo)是相對(duì)于父視圖的,然后通過上邊這段代碼,調(diào)整了相對(duì)坐標(biāo)的值,讓其變?yōu)橄鄬?duì)于子視圖啦。

絕對(duì)坐標(biāo)和相對(duì)坐標(biāo)示意圖

事件類型

涉及MotionEvent使用的代碼一般如下:

    int action = MotionEventCompat.getActionMasked(event);
    switch(action) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            break;
    }

這里就引入了關(guān)于MotionEvent的一個(gè)重要概念,事件類型。事件類型就是指MotionEvent對(duì)象所代表的動(dòng)作。比如說,當(dāng)你的一個(gè)手指在屏幕上滑動(dòng)一下時(shí),系統(tǒng)會(huì)產(chǎn)生一系列的觸摸事件對(duì)象,他們所代表的動(dòng)作有所不同。有的事件代表你手指按下這個(gè)動(dòng)作,有的事件代表你手指在屏幕上滑動(dòng),還有的事件代表你手指離開屏幕。這些事件的事件類型就分別為ACTION_DOWN,ACTION_MOVE,和ACTION_UP。上述這個(gè)動(dòng)作所產(chǎn)生的一系列事件,被稱為一個(gè)事件流,它包括一個(gè)ACTION_DOWN事件,很多個(gè)ACTION_MOVE事件,和一個(gè)ACTION_UP事件。

單個(gè)手指動(dòng)作.gif

當(dāng)然,除了這三個(gè)類型外,還有很多不同的事件類型,比如ACTION_CANCEL。它代表當(dāng)前的手勢(shì)被取消。要理解這個(gè)類型,就必須要了解ViewGroup分發(fā)事件的機(jī)制。一般來說,如果一個(gè)子視圖接收了父視圖分發(fā)給它的ACTION_DOWN事件,那么與ACTION_DOWN事件相關(guān)的事件流就都要分發(fā)給這個(gè)子視圖,但是如果父視圖希望攔截其中的一些事件,不再繼續(xù)轉(zhuǎn)發(fā)事件給這個(gè)子視圖的話,那么就需要給子視圖一個(gè)ACTION_CANCEL事件。
?其他的類型會(huì)在接下來的博文中一一解釋。

Pointer

細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),在上一節(jié)我描述用戶手指在屏幕上滑動(dòng)的例子時(shí),特地說明了手指的數(shù)量為一個(gè)。那么當(dāng)用戶兩個(gè)或者多個(gè)手指在屏幕上滑動(dòng)時(shí),系統(tǒng)又會(huì)產(chǎn)生怎樣的事件流呢?
?為了可以表示多個(gè)觸摸點(diǎn)的動(dòng)作,MotionEvent中引入了Pointer的概念,一個(gè)pointer就代表一個(gè)觸摸點(diǎn),每個(gè)pointer都有自己的事件類型,也有自己的橫軸坐標(biāo)值。一個(gè)MotionEvent對(duì)象中可能會(huì)存儲(chǔ)多個(gè)pointer的相關(guān)信息,每個(gè)pointer都會(huì)有一個(gè)自己的id和index。pointer的id在整個(gè)事件流中是不會(huì)發(fā)生變化的,但是index會(huì)發(fā)生變化。
?MotionEvent類中的很多方法都是可以傳入一個(gè)int值作為參數(shù)的,其實(shí)傳入的就是pointer的index值。比如getX(pointerIndex)getY(pointerIndex),此時(shí),它們返回的就是index所代表的觸摸點(diǎn)相關(guān)事件坐標(biāo)值。
?由于pointer的index值在不同的MotionEvent對(duì)象中會(huì)發(fā)生變化,但是id值卻不會(huì)變化。所以,當(dāng)我們要記錄一個(gè)觸摸點(diǎn)的事件流時(shí),就只需要保存其id,然后使用findPointerIndex(int)來獲得其index值,然后再獲得其他信息。

    private final static int INVALID_ID = -1;
    private int mActivePointerId = INVALID_ID;
    private int mSecondaryPointerId = INVALID_ID;
    private float mPrimaryLastX = -1;
    private float mPrimaryLastY = -1;
    private float mSecondaryLastX = -1;
    private float mSecondaryLastY = -1;
    public boolean onTouchEvent(MotionEvent event) {
        int action = MotionEventCompat.getActionMasked(event);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                int index = event.getActionIndex();
                mActivePointerId = event.getPointerId(index);
                mPrimaryLastX = MotionEventCompat.getX(event,index);
                mPrimaryLastY = MotionEventCompat.getY(event,index);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                index = event.getActionIndex();
                mSecondaryPointerId = event.getPointerId(index);
                mSecondaryLastX = event.getX(index);
                mSecondaryLastY = event.getY(index);
                break;
            case MotionEvent.ACTION_MOVE:
                index = event.findPointerIndex(mActivePointerId);
                int secondaryIndex = MotionEventCompat.findPointerIndex(event,mSecondaryPointerId);
                final float x = MotionEventCompat.getX(event,index);
                final float y = MotionEventCompat.getY(event,index);
                final float secondX = MotionEventCompat.getX(event,secondaryIndex);
                final float secondY = MotionEventCompat.getY(event,secondaryIndex);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                xxxxxx(涉及pointer id的轉(zhuǎn)換,之后的文章會(huì)講解)
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = INVALID_ID;
                mPrimaryLastX =-1;
                mPrimaryLastY = -1;
                break;
        }
        return true;
    }

除了pointer的概念,MotionEvent還引入了兩個(gè)事件類型:

  • ACTION_POINTER_DOWN:代表用戶使用一個(gè)手指觸摸到屏幕上,也就是說,在已經(jīng)有一個(gè)觸摸點(diǎn)的情況下,有新出現(xiàn)了一個(gè)觸摸點(diǎn)。
  • ACTION_POINTER_UP:代表用戶的一個(gè)手指離開了觸摸屏,但是還有其他手指還在觸摸屏上。也就是說,在多個(gè)觸摸點(diǎn)存在的情況下,其中一個(gè)觸摸點(diǎn)消失了。它與ACTION_UP的區(qū)別就是,它是在多個(gè)觸摸點(diǎn)中的一個(gè)觸摸點(diǎn)消失時(shí)(此時(shí),還有觸摸點(diǎn)存在,也就是說用戶還有手指觸摸屏幕)產(chǎn)生,而ACTION_UP可以說是最后一個(gè)觸摸點(diǎn)消失時(shí)產(chǎn)生。

那么,用戶先兩個(gè)手指先后接觸屏幕,同時(shí)滑動(dòng),然后在先后離開這一套動(dòng)作所產(chǎn)生的事件流是什么樣的呢?
?它所產(chǎn)生的事件流如下:

  • 先產(chǎn)生一個(gè)ACTION_DOWN事件,代表用戶的第一個(gè)手指接觸到了屏幕。
  • 再產(chǎn)生一個(gè)ACTION_POINTER_DOWN事件,代表用戶的第二個(gè)手指接觸到了屏幕。
  • 很多的ACTION_MOVE事件,但是在這些MotionEvent對(duì)象中,都保存著兩個(gè)觸摸點(diǎn)滑動(dòng)的信息,相關(guān)的代碼我們會(huì)在文章的最后進(jìn)行演示。
  • 一個(gè)ACTION_POINTER_UP事件,代表用戶的一個(gè)手指離開了屏幕。
  • 如果用戶剩下的手指還在滑動(dòng)時(shí),就會(huì)產(chǎn)生很多ACTION_MOVE事件。
  • 一個(gè)ACTION_UP事件,代表用戶的最后一個(gè)手指離開了屏幕
兩個(gè)手指的動(dòng)作.gif

getAction 和 getActionMasked

看到文章開頭那段代碼的同學(xué)可能會(huì)有點(diǎn)疑問:好像在很多代碼里,大家都是通過getAction獲得事件類型的,那么它和getActionMasked又有什么不同呢?
?從上一節(jié)我們可以得知,一個(gè)MotionEvent對(duì)象中可以包含多個(gè)觸摸點(diǎn)的事件。當(dāng)MotionEvent對(duì)象只包含一個(gè)觸摸點(diǎn)的事件時(shí),上邊兩個(gè)函數(shù)的結(jié)果是相同的,但是當(dāng)包含多個(gè)觸摸點(diǎn)時(shí),二者的結(jié)果就不同啦。
?getAction獲得的int值是由pointer的index值和事件類型值組合而成的,而getActionWithMasked則只返回事件的類型值
?舉個(gè)例子(注:假設(shè)了int中不同位所代表的含義,可能不是例子所中的前8位代表id,后8位代表事件類型):

getAction() returns 0x0105.
getActionMasked() will return 0x0005
其中0x0100就是pointer的index值。

一般來說,getAction() & ACTION_POINTER_INDEX_MASK就獲得了pointer的id,等同于getActionIndex函數(shù);getAction()& ACTION_MASK就獲得了pointer的事件類型,等同于getActionMasked函數(shù)。

批處理

為了效率,Android系統(tǒng)在處理ACTION_MOVE事件時(shí)會(huì)將連續(xù)的幾個(gè)多觸點(diǎn)移動(dòng)事件打包到一個(gè)MotionEvent對(duì)象中。我們可以通過getX(int)getY(int)來獲得最近發(fā)生的一個(gè)觸摸點(diǎn)事件的坐標(biāo),然后使用getHistorical(int,int)getHistorical(int,int)來獲得時(shí)間稍早的觸點(diǎn)事件的坐標(biāo),二者是發(fā)生時(shí)間先后的關(guān)系。所以,我們應(yīng)該先處理通過getHistoricalXX相關(guān)函數(shù)獲得的事件信息,然后在處理當(dāng)前的事件信息。
?下邊就是Android Guide中相關(guā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));
     }
 }

后續(xù)

之后的博文會(huì)繼續(xù)分析關(guān)于觸摸處理的幾個(gè)比較重要的類,比如OverScrollerEdgeEffect;然后會(huì)是一篇關(guān)于滑動(dòng)手勢(shì)處理代碼分析的文章。請(qǐng)大家繼續(xù)關(guān)注。

--
參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,635評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,733評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,935評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,172評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評(píng)論 1 282
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,595評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,908評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容

  • 開篇 最近在研究自定義View方面的知識(shí)。而自定義View中很重要的一塊就是View的交互。這就牽涉到本系列文章要...
    張利強(qiáng)閱讀 10,267評(píng)論 2 17
  • 一、 Android分發(fā)機(jī)制概述: Android如此受歡迎,就在于其優(yōu)秀的交互性,這其中,Android優(yōu)秀...
    IT楓閱讀 2,472評(píng)論 2 9
  • 一、前言 Android應(yīng)用的開發(fā)過程不可能不涉及到Touch事件的處理,簡(jiǎn)單地如設(shè)置OnClickListene...
    SparkInLee閱讀 7,143評(píng)論 3 20
  • 正值畢業(yè)季,作為畢業(yè)大軍中的一員常常有相熟或陌生學(xué)妹學(xué)弟問我,學(xué)姐呀,我要不要考研呢,其實(shí)不僅僅是現(xiàn)在,就在我剛考...
    angus0851閱讀 565評(píng)論 3 8
  • 寫在前面 該演講是根據(jù)國(guó)際演講會(huì)高階演講手冊(cè)《故事講述》第二單元來進(jìn)行設(shè)計(jì)的。 單元目標(biāo):用描述性的語(yǔ)言及對(duì)話講述...
    意為安閱讀 560評(píng)論 0 1