Android MotionEvent詳解

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

事件坐標的含義

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

  • getX()getY():由這兩個函數(shù)獲得的x,y值是相對的坐標值,相對于消費這個事件的視圖的左上點的坐標。
  • getRawX()getRawY():有這兩個函數(shù)獲得的x,y值是絕對坐標,是相對于屏幕的。
    ?在之前的文章中,我們曾經(jīng)分析過事件如何通過層層分發(fā),最終到達消費它的視圖手中。其中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ā)給子視圖時,getX()getY所獲得的相關(guān)坐標是如何改變的。當父視圖處理事件時,上述兩個函數(shù)獲得的相對坐標是相對于父視圖的,然后通過上邊這段代碼,調(diào)整了相對坐標的值,讓其變?yōu)橄鄬τ谧右晥D啦。

絕對坐標和相對坐標示意圖

事件類型

涉及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的一個重要概念,事件類型。事件類型就是指MotionEvent對象所代表的動作。比如說,當你的一個手指在屏幕上滑動一下時,系統(tǒng)會產(chǎn)生一系列的觸摸事件對象,他們所代表的動作有所不同。有的事件代表你手指按下這個動作,有的事件代表你手指在屏幕上滑動,還有的事件代表你手指離開屏幕。這些事件的事件類型就分別為ACTION_DOWN,ACTION_MOVE,和ACTION_UP。上述這個動作所產(chǎn)生的一系列事件,被稱為一個事件流,它包括一個ACTION_DOWN事件,很多個ACTION_MOVE事件,和一個ACTION_UP事件。

單個手指動作.gif

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

Pointer

細心的同學會發(fā)現(xiàn),在上一節(jié)我描述用戶手指在屏幕上滑動的例子時,特地說明了手指的數(shù)量為一個。那么當用戶兩個或者多個手指在屏幕上滑動時,系統(tǒng)又會產(chǎn)生怎樣的事件流呢?
?為了可以表示多個觸摸點的動作,MotionEvent中引入了Pointer的概念,一個pointer就代表一個觸摸點,每個pointer都有自己的事件類型,也有自己的橫軸坐標值。一個MotionEvent對象中可能會存儲多個pointer的相關(guān)信息,每個pointer都會有一個自己的id和index。pointer的id在整個事件流中是不會發(fā)生變化的,但是index會發(fā)生變化。
?MotionEvent類中的很多方法都是可以傳入一個int值作為參數(shù)的,其實傳入的就是pointer的index值。比如getX(pointerIndex)getY(pointerIndex),此時,它們返回的就是index所代表的觸摸點相關(guān)事件坐標值。
?由于pointer的index值在不同的MotionEvent對象中會發(fā)生變化,但是id值卻不會變化。所以,當我們要記錄一個觸摸點的事件流時,就只需要保存其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)換,之后的文章會講解)
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = INVALID_ID;
                mPrimaryLastX =-1;
                mPrimaryLastY = -1;
                break;
        }
        return true;
    }

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

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

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

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

getAction 和 getActionMasked

看到文章開頭那段代碼的同學可能會有點疑問:好像在很多代碼里,大家都是通過getAction獲得事件類型的,那么它和getActionMasked又有什么不同呢?
?從上一節(jié)我們可以得知,一個MotionEvent對象中可以包含多個觸摸點的事件。當MotionEvent對象只包含一個觸摸點的事件時,上邊兩個函數(shù)的結(jié)果是相同的,但是當包含多個觸摸點時,二者的結(jié)果就不同啦。
?getAction獲得的int值是由pointer的index值和事件類型值組合而成的,而getActionWithMasked則只返回事件的類型值
?舉個例子(注:假設(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事件時會將連續(xù)的幾個多觸點移動事件打包到一個MotionEvent對象中。我們可以通過getX(int)getY(int)來獲得最近發(fā)生的一個觸摸點事件的坐標,然后使用getHistorical(int,int)getHistorical(int,int)來獲得時間稍早的觸點事件的坐標,二者是發(fā)生時間先后的關(guān)系。所以,我們應(yīng)該先處理通過getHistoricalXX相關(guān)函數(shù)獲得的事件信息,然后在處理當前的事件信息。
?下邊就是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ù)

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

--
參考文章:

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

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

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