多點(diǎn)觸摸基本概念

導(dǎo)言

Android支持多點(diǎn)觸摸事件,平時(shí)比較常見(jiàn)的可能就是放大和縮小手勢(shì),其次常見(jiàn)的可能就是自定義一些滑動(dòng)視圖,為了避免一些“意外”出現(xiàn)(比方說(shuō)瞬移),還是考慮一下多點(diǎn)觸摸

基本概念

Android對(duì)于事件的概念都封裝在了MotionEvent這個(gè)類中,首先了解一些基本的信息

public static final int ACTION_MASK             = 0xff;
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;
public static final int ACTION_POINTER_INDEX_SHIFT = 8;

public final int getAction() {
    return nativeGetAction(mNativePtr);
}

public final int getActionMasked() {
    return nativeGetAction(mNativePtr) & ACTION_MASK;
}

public final int getActionIndex() {
    return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
                >> ACTION_POINTER_INDEX_SHIFT;
}

對(duì)比上面的三個(gè)方法,舉個(gè)例子看看:
getAction()本身返回的是一個(gè)32位的int,即
0000 0001 0000 0011
getActionMasked()進(jìn)行了低8位與運(yùn)算,從而結(jié)果只會(huì)剩下低8位
0000 0000 0000 0011
getActionIndex()進(jìn)行了高8位與運(yùn)算,然后再右移8位,相當(dāng)于把高8位移到低8位,然后高8位填0
0000 0001 0000 0000
0000 0000 0000 0001

從上述計(jì)算可以看出,實(shí)際上getAction()返回的是ActionIndex和事件類型的組合
ActionIndex:
取值為0、1、2...N,在手指的移出后Index還會(huì)進(jìn)行自動(dòng)調(diào)整,始終保持0、1...手指數(shù)量-1這個(gè)序列,也就是說(shuō)index=0只能標(biāo)識(shí)有一個(gè)手指,但是不能說(shuō)明這個(gè)手指一定是第一次按下的手指,同樣也可以是從兩根手指的情況下,松開(kāi)第一次按下的手指從而剩下的那個(gè)手指
對(duì)應(yīng)于這個(gè)還有一個(gè)概念

public final int getPointerId(int pointerIndex) {
    return nativeGetPointerId(mNativePtr, pointerIndex);
}

pointerId:
區(qū)別于ActionIndex,pointerId能夠做到保證同一手指的值不變,也就是說(shuō)可以通過(guò)pointerId來(lái)標(biāo)識(shí)具體的手指

小結(jié)

方法 描述
getAction() 獲得ActionIndex和事件類型組合的值,當(dāng)且僅當(dāng)只有一個(gè)手指,此時(shí)獲得的是事件類型,那么也就不能處理多點(diǎn)觸摸
getActionMasked() 獲得事件類型
getActionIndex() 獲得事件Index

個(gè)人來(lái)說(shuō),推薦在OnTouchEvent中使用getActionMasked()來(lái)判斷事件類型,這樣才是符合MotionEvent的設(shè)計(jì)原理

多點(diǎn)觸摸實(shí)踐

我們知道處理觸摸事件的邏輯是在onTouchEvent(MotionEvent event)中處理
也就是說(shuō)每一次只能處理單獨(dú)的一個(gè)MotionEvent事件
這里考慮一個(gè)滑動(dòng)視圖,比方說(shuō)
https://github.com/dda135/PullRefreshLayout這一個(gè)布局刷新自定義控件

首先明確幾個(gè)概念:
1.滑動(dòng)的時(shí)候攔截事件的可能是任何一根手指
2.滑動(dòng)以最后一個(gè)按下來(lái)的手指為準(zhǔn)
3.最后一個(gè)滑動(dòng)的手指松開(kāi)之后,后續(xù)的滑動(dòng)應(yīng)該盡量以這個(gè)手指之前按下的最后一個(gè)手指為準(zhǔn),如果這個(gè)手指之前已經(jīng)松開(kāi),那么再往前追溯

概念明確之后,實(shí)現(xiàn)思路也就清晰了:
1.應(yīng)該記錄當(dāng)前最后一個(gè)手指的pointerId
2.應(yīng)該提供一個(gè)列表存儲(chǔ)按照手指按下的順序存儲(chǔ)pointerId

private static final int INVALID_POINTID = -1;
//用于記錄上一次事件的坐標(biāo)
private Point mLastPoint;
//用于記錄當(dāng)前最后一個(gè)手指的pointerId
private int mActivePointId;
//用于按順序存儲(chǔ)按下手指的pointerId
private List<Integer> mPointIdList = new ArrayList<>();

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        ...
        int pointerIndex = event.getActionIndex();
        int pointerId = event.getPointerId(pointerIndex);
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                mActivePointId = pointerId;
                mLastPoint.set((int) event.getX(pointerIndex), (int) event.getY(pointerIndex));
                mPointIdList.add(pointerId);
                break;
            case MotionEvent.ACTION_MOVE:
                int activeIndex = event.findPointerIndex(mActivePointId);
                if(activeIndex < 0){
                    return false;
                }
                int x = (int) event.getX(activeIndex);
                int y = (int) event.getY(activeIndex);
                int deltaY = (y - mLastPoint.y);
                int dy = Math.abs(deltaY);
                int dx = Math.abs(x - mLastPoint.x);     
                if (dy > mTouchSlop && dy >= dx) {
                    canUp = mOption.canUpToDown();
                    canDown = mOption.canDownToUp();
                    canUpIntercept = (deltaY > 0 && canUp);
                    canDownIntercept = (deltaY < 0 && canDown);
                    return canUpIntercept || canDownIntercept;
                }
                return false;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointId = INVALID_POINTID;
                mPointIdList.clear();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                resetTouch(event);
                break;
            default:
                break;
        }
        return false;
    }

    /**
     * 通過(guò)列表結(jié)構(gòu)進(jìn)行手指回溯操作
     * @param event 當(dāng)前事件對(duì)象
     */
    private void resetTouch(MotionEvent event){
        int currentReleaseIndex = event.getActionIndex();
        int currentReleaseId = event.getPointerId(currentReleaseIndex);
        mPointIdList.remove((Integer) currentReleaseId);//先移除當(dāng)前松開(kāi)的手指
        while(mPointIdList.size() > 0){
            mActivePointId = mPointIdList.get(mPointIdList.size() - 1);
            int pointIndex = event.findPointerIndex(mActivePointId);
            if (pointIndex < 0){//當(dāng)前Id無(wú)效,廢棄
                mPointIdList.remove(mPointIdList.size() - 1);
                continue;
            }
            mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));//當(dāng)前活動(dòng)手指變化,記錄當(dāng)前活動(dòng)的最新坐標(biāo)
            return;
        }
        mActivePointId = INVALID_POINTID;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        ...
        int pointIndex = event.getActionIndex();
        int pointId = event.getPointerId(pointIndex);
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                mActivePointId = pointId;
                mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));
                mPointIdList.add(mActivePointId);
                break;
            case MotionEvent.ACTION_MOVE:
                int activePointIndex = event.findPointerIndex(mActivePointId);
                if(activePointIndex < 0){
                    return false;
                }
                isOnTouch = true;
                updatePos((int) (mOption.getMoveRatio() * (event.getY(activePointIndex) - mLastPoint.y)));
                mLastPoint.set((int)event.getX(activePointIndex),(int)event.getY(activePointIndex));
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mActivePointId = INVALID_POINTID;
                mPointIdList.clear();
                ...
                break;
            case MotionEvent.ACTION_POINTER_UP:
                resetTouch(event);
                break;
            default:
                break;
        }
        return true;
    }

實(shí)際上思路很簡(jiǎn)單,當(dāng)手指按下的時(shí)候(單指或多指),標(biāo)記當(dāng)前手指為活動(dòng)手指,MOVE事件的時(shí)候以活動(dòng)手指的移動(dòng)為準(zhǔn),當(dāng)有手指松開(kāi)的時(shí)候,嘗試尋找新的活動(dòng)手指即可。

總結(jié)

Android源碼的一些例子可以參考RecyclerView等滑動(dòng)視圖的源碼,里面也有一些類似的操作,多點(diǎn)觸摸實(shí)際上和單點(diǎn)觸摸差不多,只要明確基本概念,處理起來(lái)也會(huì)相對(duì)輕松

?著作權(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)容

  • 開(kāi)篇 最近在研究自定義View方面的知識(shí)。而自定義View中很重要的一塊就是View的交互。這就牽涉到本系列文章要...
    張利強(qiáng)閱讀 10,504評(píng)論 2 17
  • View的事件分發(fā)機(jī)制 View的事件分發(fā)機(jī)制簡(jiǎn)單來(lái)說(shuō)就是將用戶與手機(jī)屏幕的交互事件交由正確的控件進(jìn)行處理,從而可...
    蕉下孤客閱讀 950評(píng)論 0 4
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,029評(píng)論 25 709
  • 一、 Android分發(fā)機(jī)制概述: Android如此受歡迎,就在于其優(yōu)秀的交互性,這其中,Android優(yōu)秀...
    IT楓閱讀 2,650評(píng)論 2 9
  • 先有電閃 還是先有雷鳴 我一直都沒(méi)有弄清 隔著窗玻璃 看半空的金光四射 轟隆隆 轟隆隆 雨灑漫天 飛落彈起屋檐 ...
    14432c3ec397閱讀 323評(píng)論 8 7

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