多手指觸控,其實也不是很難

多點觸控,一直以來都是事件處理中比較晦澀的一個話題。其一是由於它的機制與咱們常規思惟有點不一樣,基二是由於咱們用的比較少。那麼做爲一個有點追求的Android開發者,咱們必需要掌握這些,這樣能夠提升代碼的逼格。java

寫這篇文章仍是有點難度的,我反反覆覆修改了好屢次,真的是刪了又改,改了又刪,只爲把多點觸控講得明明白白。最後我決定把本文分爲三部分進行講解git

  1. 講解多手指觸摸的一些關鍵性概念。雖然這部分概念很是抽象,而且也沒法用源碼去解釋(源碼在底層),可是這部分概念是最關鍵的。若是你想掌握多點觸控,必須理解並記住這些概念。
  2. 講解多手指觸摸事件在ViewGroup是如何分發處理的。由於只有理解了這個,咱們才能寫出正確的多手指觸摸事件的代碼。
  3. 經過一個例子講解如何在滑動控件中支持多手指滑動。

好了,廢話很少說了,讓咱們開始此次激情的旅程吧。github

觸摸事件

首先咱們從MotionEvent.getAction()講起吧。不少地方把這個方法的返回值叫作觸摸事件的類型,其實這個叫法是錯誤的,它的返回值不只包含事件的類型,還包含手指的索引值。ide

假如MotionEvent.getAction()返回一個值,用十六進制表示爲0X0100,這個值的高八位的值是01,用二進制表示就是0000 0001,它表示手指的索引,而低八位的值是00,用二進制表示就是0000 0000,它才表示事件的類型。源碼分析

事件類型

那麼咱們怎麼獲取這個事件的類型呢,我想你們應該都想到了事件類型的掩碼,MotionEvent.getActionMask()就是經過事件類型掩碼獲取事件類型的。post

那麼,爲何你們一直說MotionEvent.getAction()返回的就是事件類型呢?由於這是一個巧合,對於單手指操做,MotionEvent.getAction()的返回值中,高八位的索引值是0,所以它正好與事件類型的值同樣。spa

對於支持多手指操做,MotionEvent.getAction()返回值的事件索引就再也不一直是0了,它會隨着手指的增長而改變,所以MotionEvent.getActionMask()纔是返回事件類型的正確操做。3d

那麼咱們來看下,多手指觸摸狀況下所支持的事件類型code

事件類型 事件說明
ACTION_DOWN 第一個手指按下
ACTION_POINTER_DOWN 其它手指按下
ACTION_MOVE 手指移動
ACTION_POINTER_UP 不是最後一個手指擡起
ACTION_UP 最後一個手指擡起

咱們經過一個例子來解釋下這幾個事件的觸發時機。orm

  1. 當第一個手指按下的時候,此時觸發的事件類型是ACTION_DOWN
  2. 當有第二個,甚至更多的手指按下的時候就會觸發ACTION_POINTER_DOWN事件。
  3. 當任意一個手指滑動的時候,就會觸發ACTION_MOVE事件。
  4. 當不是最後一個手指擡起時,會觸發ACTION_POINTER_UP事件。
  5. 當最後一個手指擇時,會觸發ACTION_UP事件。

手指索引

MotionEvent.getAction()返回值中還有個神祕的手指索引,它能夠經過MotionEvent.getActionIndex()獲取。那麼它有啥用呢?對於單手指,沒有任何叼用,可是對於多手指,那它的做用就大了,這能夠獲取手指的觸摸事件的信息,例如MotionEvent.getX(int pointerIndex)獲取X座標值。

手指ID

剛纔在事件類型部分,不知你們有沒有注意到,ACTION_MOVE是不區分手指的,那麼咱們怎麼知道是哪一個手指觸發了ACTION_MOVE的呢?你是否是第一時間想到了手指索引?請你放棄這個想法!

人能夠經過眼睛觀察到手指的按下順序,可是硬件和軟件是沒法作到的,而手指的索引在事件中可能會改變的。那麼一個嚴峻的問題來了,如何跟蹤一個手指呢?用PointerId!至於原理是什麼,我也不太清楚。

那麼怎麼獲取一個手指的PointerId呢?當遇到ACTION_DOWNACTION_POINTER_DOWN的時候,經過以下代碼獲取

// 獲取手指的索引 
int pointerIndex = motionEvent.getActionIndex();
// 經過手指索引獲取手指ID
int pointerId = motionEvent.getPointerId(pointerIndex);
複製代碼

在前面的手指索引部分,咱們知道經過索引可能獲取事件的信息,例如座標值,以下代碼

// 獲取手指索引
        int pointerIndex = event.getActionIndex();
        // 獲取座標值
        float x = event.getX(pointerIndex);
        float y = event.getY(pointerIndex);
複製代碼

然而在ACTION_MOVE事件中,咱們要獲取某個手指的座標值,怎麼辦呢?首先咱們要保存在ACTION_DOWNACTION_POINTER_DOWN中保存手指PointerId值,而後經過這個PointerId調用MotionEvent.findPointerIndex(int pointerId)獲取手指索引值,最後經過索引值獲取座標值,代碼以下

case MotionEvent.ACTION_MOVE:
    // 根據PointerId獲取某個手指的索引 
    int pointerIndex = event.findPointerIndex(mPrimaryPointerId);
    // 獲取座標值
    float x = event.getX(pointerIndex);
    float y = event.getY(pointerIndex);
    break;
複製代碼

多手指事件處理

對於多手指觸摸事件呢,其實比單手指只是多出了ACTION_POINTER_DOWNACTION_POINTER_UP兩個事件,那麼這兩個事件在ViewGroup中是如何分發處理的呢?若是要用源碼來分析呢,這篇文章的篇幅就太長了,可是呢,恰巧這兩個事件與ACTION_MOVE的分發處理流程是同樣的。若是你還不懂ACTION_MOVE是如何分發處理的,能夠參考我以前寫的ViewGroup事件分發和處理源碼分析

支持多手指的滑動控件

掌握了前面的基礎知識後,咱們如今就又到了喜聞樂見的實戰環節,在這一部分,咱們要使一個滑動控件支持多手指滑動。

在實現這個功能以前,咱們要明確實現思路

  1. 只有主手指能控制控件的滑動。
  2. 若是有手指按下,就認爲這個手指是主手指。
  3. 當有手指擡起時,若是是主手指,那就必須從新找一個手指做爲新的主手指。

首先咱們須要一個可滑動的控件,這個控件取自手把手教你如何寫事件處理的代碼這篇文章的滑動控件,而且我須要你們對這篇文章的講的事件處理能理解清楚,由於下面寫的代碼,我不會去解釋這些基本知識。

咱們前面說過,ACTION_POINTER_DOWNACTION_POINTER_UP的處理流程是和ACTION_MOVE同樣的,那麼要不要截斷呢?那就要看當遇到這兩個事件的時候咱們要作什麼。

根據實現思路中的第二條,若是有手指按下,就認爲是主手指,所以在處理ACTION_POINTER_DOWN時候只是簡單獲取手指的PointerId,而後保存爲主手指便可,因此不須要去截斷。

根據實現思路的第三條,若是擡起的是主手指,那麼就要從新找一個替代的手指做爲主手指,因此也不須要去截斷。

那麼,在onInterceptTouchEvent()onTouchEvent()的處理方式是同樣的,首先咱們看下保存主手指的代碼以下

public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                onPrimaryPointerDown(ev);
                break;

            case MotionEvent.ACTION_POINTER_DOWN:
                onPrimaryPointerDown(ev);
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_POINTER_DOWN:
                onPrimaryPointerDown(event);
                break;
        }
        return true;
    }
    
    
    /** * 當有新手指按下的時候,就認做是主手指,因而從新記錄按下點的座標,以及更新最新的X座標。 * * @param event 觸摸事件。 */
    private void onPrimaryPointerDown(MotionEvent event) {
        // 獲取手指索引
        int pointerIndex = event.getActionIndex();
        // 經過手指索引獲取手指ID
        mPrimaryPointerId = event.getPointerId(pointerIndex);
        // 經過手指索引保存座標值
        mLastX = mStartX = event.getX(pointerIndex);
        mStartY = event.getY(pointerIndex);
    }    
複製代碼

而後,咱們來看下當有主手指擡起時,如何尋找替代的主手指

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_POINTER_UP:
                onPrimaryPointerUp(ev);
                break;

        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_POINTER_UP:
                onPrimaryPointerUp(event);
                break;
        }
        return true;
    }
    
    /** * 當主手指擡起時,尋找一個新的主手指,而且更新最新的X座標值爲新主手指的X座標值。 * * @param event */
    private void onPrimaryPointerUp(MotionEvent event) {
        // 獲取擡起手指的索引值
        int pointerIndex = event.getActionIndex();
        // 經過索引值,獲取擡起手指的ID
        int pointerId = event.getPointerId(pointerIndex);
        // 若是擡起手指的ID等於主手指的ID
        if (pointerId == mPrimaryPointerId) {
            // 尋找一個已經存在的手指索引
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            // 經過新的手指索引獲取手指ID
            mPrimaryPointerId = event.getPointerId(newPointerIndex);
            // 經過新的手指索引獲取座標值
            mLastX = event.getX(newPointerIndex);
        }
    }    
複製代碼

把這些問題解決後,那麼在處理滑動的代碼的時候,就要經過這個主手指ID來獲取座標值,而後根據這些座標值來決定滑動,我這裏用部分代碼來演示下

public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // 獲取主手指的座標值
                PointF primaryPointerPoint = getPrimaryPointerPoint(ev);
                // 根據座標值判斷是否須要滑動
                if (canScroll(primaryPointerPoint.x, primaryPointerPoint.y)) {
                    mBeingDragged = true;
                    getParent().requestDisallowInterceptTouchEvent(true);
                    // 執行一次滑動
                    performDrag(primaryPointerPoint.x);
                    mLastX = primaryPointerPoint.x;
                    // 能夠滑動就截斷事件
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    
    /** * 獲取主手指在某個事件觸發時的座標。 * * @param event 觸摸事件。 * @return 若是成功,返回座標點,不然返回null。 */
    private PointF getPrimaryPointerPoint(MotionEvent event) {
        PointF pointF = null;
        if (mPrimaryPointerId != INVALID_POINTER_ID) {
            int pointerIndex = event.findPointerIndex(mPrimaryPointerId);
            if (pointerIndex != -1) {
                pointF = new PointF(event.getX(pointerIndex), event.getY(pointerIndex));
            }
        }
        return pointF;
    }    
複製代碼

總結

要掌握多手指滑動,必須先得掌握其關鍵的概念,有了這些概念咱們就能夠知道事件什麼時候觸發,怎麼跟蹤一個手指。而後咱們須要掌握多手指事件的處理流程,巧合的是,只要知道ACTION_MOVE的處理流程就明白了多手指事件的流程。最後咱們要掌握爲一個滑動控件添加多手指支持的實現思路。

有了這三步,基本上就能夠實現一個支持多手指滑動的控件。不過請注意個人措辭,是基本上,是基本上,是基本上!

最後,我默默地留下一個github地址,供你們參考。

相關文章
相關標籤/搜索