Android手勢檢測——GestureDetector全面分析

出處
炎之鎧郵箱:yanzhikai_yjk@qq.com
博客地址:http://blog.csdn.net/totond
本文原創(chuàng),轉(zhuǎn)載請注明本出處!

前言

在很多視頻播放器中,都存在使用不同的手勢來控制進度、亮度\音量和暫停播放等功能。Android提供了一個GestureDetector來幫助我們識別一些基本的觸摸手勢(還有ScaleGestureDetector可以識別縮放手勢),讓我們很方便地實現(xiàn)手勢控制功能。下面我們就來學(xué)習(xí)一下GestureDetector的使用和通過源碼(Android7.0)來分析一下它的實現(xiàn),讓我們對觸摸事件處理的理解更加深入。

GestureDetector介紹

Detector的意思就是探測者,所以GestureDetector就是用來監(jiān)聽手勢的發(fā)生。它內(nèi)部有3個Listener接口,用來回調(diào)不同類型的觸摸事件,用一個簡略的類圖來顯示:


  
  里面這些接口的方法,就是相應(yīng)觸摸事件的回調(diào),實現(xiàn)了這些方法,就能實現(xiàn)傳入觸摸事件之后做出相應(yīng)的回調(diào)。

一些回調(diào)接口:

1.OnGestureListener,這個Listener監(jiān)聽一些手勢,如單擊、滑動、長按等操作:

  • onDown(MotionEvent e):用戶按下屏幕的時候的回調(diào)。
  • onShowPress(MotionEvent e):用戶按下按鍵后100ms(根據(jù)Android7.0源碼)還沒有松開或者移動就會回調(diào),官方在源碼的解釋是說一般用于告訴用戶已經(jīng)識別按下事件的回調(diào)(我暫時想不出有什么用途,因為這個回調(diào)觸發(fā)之后還會觸發(fā)其他的,不像長按)。
  • onLongPress(MotionEvent e):用戶長按后(好像不同手機的時間不同,源碼里默認是100ms+500ms)觸發(fā),觸發(fā)之后不會觸發(fā)其他回調(diào),直至松開(UP事件)。
  • onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY):手指滑動的時候執(zhí)行的回調(diào)(接收到MOVE事件,且位移大于一定距離),e1,e2分別是之前DOWN事件和當前的MOVE事件,distanceX和distanceY就是當前MOVE事件和上一個MOVE事件的位移量。
  • onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY):用戶執(zhí)行拋操作之后的回調(diào),MOVE事件之后手松開(UP事件)那一瞬間的x或者y方向速度,如果達到一定數(shù)值(源碼默認是每秒50px),就是拋操作(也就是快速滑動的時候松手會有這個回調(diào),因此基本上有onFling必然有onScroll)。
  • onSingleTapUp(MotionEvent e):用戶手指松開(UP事件)的時候如果沒有執(zhí)行onScroll()onLongPress()這兩個回調(diào)的話,就會回調(diào)這個,說明這是一個點擊抬起事件,但是不能區(qū)分是否雙擊事件的抬起。

2.OnDoubleTapListener,這個Listener監(jiān)聽雙擊和單擊事件。

  • onSingleTapConfirmed(MotionEvent e):可以確認(通過單擊DOWN后300ms沒有下一個DOWN事件確認)這不是一個雙擊事件,而是一個單擊事件的時候會回調(diào)。
  • onDoubleTap(MotionEvent e):可以確認這是一個雙擊事件的時候回調(diào)。
  • onDoubleTapEvent(MotionEvent e):onDoubleTap()回調(diào)之后的輸入事件(DOWN、MOVE、UP)都會回調(diào)這個方法(這個方法可以實現(xiàn)一些雙擊后的控制,如讓View雙擊后變得可拖動等)。

3.OnContextClickListener,很多人都不知道ContextClick是什么,我以前也不知道,直到我把平板接上了外接鍵盤——原來這就是鼠標右鍵。。。

  • onContextClick(MotionEvent e):當鼠標/觸摸板,右鍵點擊時候的回調(diào)。

4.SimpleOnGestureListener,實現(xiàn)了上面三個接口的類,擁有上面三個的所有回調(diào)方法。

  • 由于SimpleOnGestureListener不是抽象類,所以繼承它的時候只需要選取我們所需要的回調(diào)方法來重寫就可以了,非常方便,也減少了代碼量,符合接口隔離原則,也是模板方法模式的實現(xiàn)。而實現(xiàn)上面的三個接口中的一個都要全部重寫里面的方法,所以我們一般都是選擇SimpleOnGestureListener。

ps:上面所有的回調(diào)方法的返回值都是boolean類型,和View的事件傳遞機制一樣,返回true表示消耗了事件,flase表示沒有消耗。

GestureDetector的使用

GestureDetector的使用很簡單,因為它的功能就是定義為識別手勢,所以使用的話就是輸入完整的觸摸事件(完整的意思就是用戶所有的觸摸操作都是輸入給它。為什么要強調(diào)完整,因為我上一篇博客就是分享如何攔截子View的部分觸摸事件),識別然后進行相應(yīng)的回調(diào):

    private void init(Context context){
        mOnGestureListener = new MyOnGestureListener();
        mGestureDetector = new GestureDetector(context,mOnGestureListener);
//        mGestureDetector.setIsLongpressEnabled(false);
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //監(jiān)聽觸摸事件
                return mGestureDetector.onTouchEvent(event);
            }
        });
        
        setOnGenericMotionListener(new OnGenericMotionListener() {
            @Override
            public boolean onGenericMotion(View v, MotionEvent event) {
                Log.d(TAG, "onGenericMotion: ");
                //監(jiān)聽鼠標右鍵點擊事件
                return mGestureDetector.onGenericMotionEvent(event);
            }
        });
    }

如上面的代碼,要使用OnGestureListener和OnDoubleTapListener里面的回調(diào)需要調(diào)用GestureDetector.onTouchEvent()方法,而使用OnContextClickListener的話則是需要調(diào)用onGenericMotionEvent()方法,注意一個是在onTouch()方法一個是在onGenericMotion()方法。

看完了上面一堆文字,其實你就會懂得如何使用GestureDetector了,但是如果你想了解它的回調(diào)的時機為什么會是這樣的,想具體了解它們的回調(diào)時機,可以繼續(xù)看下去,下面是源碼分析。

GestureDetector源碼分析

1. 初始化處理

GestureDetector的源碼接近800行,這在Android源碼中已經(jīng)算是比較短的了(畢竟注釋也占一兩百行了),所以說它的實現(xiàn)也不是很復(fù)雜的。從它的構(gòu)造方法開始:

    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }

    public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        //初始化Handler
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        //設(shè)置Listener
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        if (listener instanceof OnContextClickListener) {
            setContextClickListener((OnContextClickListener) listener);
        }

        init(context);
    }

    private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;

        // Fallback to support pre-donuts releases
        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
        if (context == null) {
            //相當于下面的getScaledTouchSlop,表示滑動的時候,手的移動要大于這個距離才開始移動控件
            touchSlop = ViewConfiguration.getTouchSlop();
            //相當于下面的getScaledDoubleTapTouchSlop,表示點擊的時候,手指移動大于這個距離,就被認為不可能是雙擊
            doubleTapTouchSlop = touchSlop; 
            //相當于下面的getScaledDoubleTapSlop,表示第二次點擊的時候,和第一次的點擊點位置距離如果大于這個,就被認為不是雙擊
            doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
            mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
            mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
        } else {
            final ViewConfiguration configuration = ViewConfiguration.get(context);
            touchSlop = configuration.getScaledTouchSlop();
            doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
            doubleTapSlop = configuration.getScaledDoubleTapSlop();
            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        }
        //做平方好計算距離,后面的距離對比也是用平方
        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

可見GestureDetector的創(chuàng)建就是初始化一些屬性,然后就是把對應(yīng)的Listener設(shè)置好,還有初始化Handler,而這里的GestureHandler,是控制onShowPress(),onLongPress(),onSingleTapConfirmed()`回調(diào)的關(guān)鍵:

  private class GestureHandler extends Handler {
        //省略構(gòu)造函數(shù)...

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_PRESS:
                mListener.onShowPress(mCurrentDownEvent);
                break;
                
            case LONG_PRESS:
                dispatchLongPress();
                break;
                
            case TAP:
                // If the user's finger is still down, do not count it as a tap
                //這里控制SingleTapConfirmed的回調(diào),
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        //如果已經(jīng)松開,就立刻調(diào)用SingleTapConfirmed
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        //如果處理Message的時候還沒松開,就設(shè)置mDeferConfirmSingleTap為true,在UP事件的時候調(diào)用SingleTapConfirme
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

            default:
                throw new RuntimeException("Unknown message " + msg); //never
            }
        }
    }

    //長按處理
    private void dispatchLongPress() {
        mHandler.removeMessages(TAP);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }

2. 輸入處理

初始化完之后,就是看它的如何處理輸入了,這是它的核心邏輯:

    public boolean onTouchEvent(MotionEvent ev) {
        //檢查事件輸入的一致性,log出來一致性的信息,如:有事件只有up沒有down
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
        }

        final int action = ev.getAction();

        //開始速度檢測
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);


        //檢測是否非主要指針抬起動作(如果是多點觸摸)
        final boolean pointerUp =
                (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;

        final int skipIndex = pointerUp ? ev.getActionIndex() : -1;

        // Determine focal point
        // 是非主要指針抬起動作的話會跳過
        float sumX = 0, sumY = 0;
        final int count = ev.getPointerCount();
        //把所有還在觸摸的手指的位置x,y加起來,后面求平均數(shù),算出中心焦點
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;
            sumX += ev.getX(i);
            sumY += ev.getY(i);
        }
        final int div = pointerUp ? count - 1 : count;
        final float focusX = sumX / div;
        final float focusY = sumY / div;

        boolean handled = false;

        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            //...
            break;

        case MotionEvent.ACTION_POINTER_UP:
            //...
            break;

        case MotionEvent.ACTION_DOWN:
            //...
            break;

        case MotionEvent.ACTION_MOVE:
            //...
            break;

        case MotionEvent.ACTION_UP:
            //...
            break;

        case MotionEvent.ACTION_CANCEL:
            cancel();
            break;
        }

        //對未被處理的事件進行一次一致性檢測
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
        }
        return handled;
    }

上面的注釋寫得很清楚了,主要onTouchEvent()的主要思路就是先對輸入事件做出統(tǒng)一處理,提取一些共有的信息,如多個點同時觸摸時候的中心焦點和滑動速度等,然后根據(jù)事件的分類做出相應(yīng)的處理。

ps:InputEventConsistencyVerifier對輸入事件進行的一致性檢測的結(jié)果并不影響GestureDetector的運行,如果檢測到一致性不符合的事件(只有UP事件而前面沒有DOWN事件),就只會輸出log告訴開發(fā)者。

2.1. DOWN事件處理

下面進入DOWN事件的處理:

    //...
        case MotionEvent.ACTION_DOWN:
            if (mDoubleTapListener != null) {
                //處理雙擊
                //取消TAP事件
                boolean hadTapMessage = mHandler.hasMessages(TAP);
                if (hadTapMessage) mHandler.removeMessages(TAP);
                if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                    // This is a second tap
                    mIsDoubleTapping = true;
                    // Give a callback with the first tap of the double-tap
                    //回調(diào)雙擊
                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                    // Give a callback with down event of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else {
                    // This is a first tap
                    //延時發(fā)出單擊事件,如果到了時間(300ms)還沒有取消的話就確認是TAP事件了
                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                }
            }

            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            //重置mCurrentDownEvent
            if (mCurrentDownEvent != null) {
                mCurrentDownEvent.recycle();
            }
            mCurrentDownEvent = MotionEvent.obtain(ev);
            mAlwaysInTapRegion = true;
            mAlwaysInBiggerTapRegion = true;
            mStillDown = true;
            mInLongPress = false;
            mDeferConfirmSingleTap = false;
            //處理長按
            if (mIsLongpressEnabled) {
                mHandler.removeMessages(LONG_PRESS);
                //延時發(fā)送長按事件
                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
            }
            //延時發(fā)送showPress事件
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            handled |= mListener.onDown(ev);
            break;
    //...

    //判斷第二次點擊是否有效雙擊
    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
            MotionEvent secondDown) {
        //第一次點擊后是否有移動超出范圍
        if (!mAlwaysInBiggerTapRegion) {

            return false;
        }

        final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
        if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
            return false;
        }

        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
        //判斷第二次點擊是否在附近,在附近才被認為是雙擊
        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    }

可見,對DOWN事件涉及:

  • 處理單擊判斷:如果收到一次DOWN事件,而且前段時間沒有DOWN事件的話,會發(fā)送一個延時的TAP信息,而一段時間(300ms)之后沒有被取消的話,就執(zhí)行GestureHandler里面的TAP單擊確認操作。
  • 處理雙擊判斷:如果前面也有一次DOWN事件,而且也符合isConsideredDoubleTap()的條件(第一次點擊后沒有移動超出范圍,第二次點擊也在附近),就可以確認雙擊,執(zhí)行onDoubleTap()onDoubleTapEvent()的回調(diào)。
  • 處理長按判斷:先看用戶是否允許檢測長按,然后就是發(fā)送一個延時的LONG_PRESS信息,如果到時候還沒被取消的話就是回調(diào)長按方法了。
  • 處理showPress判斷:這個和長按差不多,就是時間(100ms)短了一點而已。

PS:handled是boolean變量,|=符號是用符號右邊的值跟左邊的值進行或運算再賦值給左邊。a |= b等價于a = a | b,這里使handled變量初始值為false,進行了多次|=操作,一旦有結(jié)果是true的話,handled最后的值就是true,所有結(jié)果都是false最后才會false,onTouchEvent()方法最后返回這個handled,就可以表示事件有沒被處理,實現(xiàn)了對事件處理的封裝。

2.2. MOVE事件處理

然后再看看對MOVE事件的處理:

    //...
        case MotionEvent.ACTION_MOVE:
            //如果是正在長按和點擊了鼠標右鍵
            if (mInLongPress || mInContextClick) {
                break;
            }
            final float scrollX = mLastFocusX - focusX;
            final float scrollY = mLastFocusY - focusY;
            if (mIsDoubleTapping) {
                // Give the move events of the double-tap
                //如果是第二次點擊的話,把移動事件也當作雙擊,有點奇怪
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mAlwaysInTapRegion) {
                //down才會使mAlwaysInTapRegion為true
                final int deltaX = (int) (focusX - mDownFocusX);
                final int deltaY = (int) (focusY - mDownFocusY);
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                //mTouchSlopSquare是一個距離的平方,表示滑動的時候,手的移動要大于這個距離才認為是Scroll事件
                if (distance > mTouchSlopSquare) {
                    //進入Scroll模式
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastFocusX = focusX;
                    mLastFocusY = focusY;
                    mAlwaysInTapRegion = false;
                    mHandler.removeMessages(TAP);
                    mHandler.removeMessages(SHOW_PRESS);
                    mHandler.removeMessages(LONG_PRESS);
                }
                if (distance > mDoubleTapTouchSlopSquare) {
                    //如果移動距離超過允許范圍,則不再可能認為移動事件是雙擊
                    mAlwaysInBiggerTapRegion = false;
                }
            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                //后續(xù)的Scroll移動,前面的是進入Scroll移動
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
            }
            break;
    //...

可見,對MOVE事件涉及:

  • onDoubleTapEvent()回調(diào):只要確認是雙擊之后,mIsDoubleTapping為true,除了長按,后面的MOVE事件都會只回調(diào)onDoubleTapEvent()。
  • onScroll()回調(diào):當MOVE不是長按,不是DoubleTapEvent之后,當移動距離大于一定距離之后,就會進入Scroll模式,然后兩個MOVE事件的位移距離scrollX或者scrollY大于1px,都會調(diào)用onScroll()

2.3. UP事件的處理

接下來再看UP事件:

    //...
        case MotionEvent.ACTION_UP:
            mStillDown = false;
            MotionEvent currentUpEvent = MotionEvent.obtain(ev);
            if (mIsDoubleTapping) {
                // Finally, give the up event of the double-tap
                //雙擊事件
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mInLongPress) {
                //長按結(jié)束
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
                handled = mListener.onSingleTapUp(ev);
                //處理單擊確認,具體邏輯看GestureHandler如何處理TAP事件
                if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                    mDoubleTapListener.onSingleTapConfirmed(ev);
                }
            } else if (!mIgnoreNextUpEvent) {
                //處理Fling,如果速度大于定義的最小速度(50),就回調(diào)Fling
                // A fling must travel the minimum tap distance
                final VelocityTracker velocityTracker = mVelocityTracker;
                final int pointerId = ev.getPointerId(0);
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                final float velocityY = velocityTracker.getYVelocity(pointerId);
                final float velocityX = velocityTracker.getXVelocity(pointerId);

                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
                }
            }
            //重置mPreviousUpEvent
            if (mPreviousUpEvent != null) {
                mPreviousUpEvent.recycle();
            }
            // Hold the event we obtained above - listeners may have changed the original.
            mPreviousUpEvent = currentUpEvent;
            //回收mVelocityTracker
            if (mVelocityTracker != null) {
                // This may have been cleared when we called out to the
                // application above.
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            mIsDoubleTapping = false;
            mDeferConfirmSingleTap = false;
            mIgnoreNextUpEvent = false;
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
            break;
    //...

可見,對MOVE事件涉及:

  • onDoubleTapEvent()回調(diào):只要確認是雙擊之后,mIsDoubleTapping為true,除了長按,后面的MOVE事件都會只回調(diào)onDoubleTapEvent()。
  • onSingleTapUp()回調(diào):DOWN事件之后沒有MOVE,或者MOVE的距離沒有超出范圍,mAlwaysInTapRegion才不會變成false,回調(diào)onSingleTapUp()。
  • onSingleTapConfirmed()回調(diào):從前面GestureHandler里面的TAP消息的實現(xiàn)可以看到:
            case TAP:
                // If the user's finger is still down, do not count it as a tap
                //這里控制SingleTapConfirmed的回調(diào),
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        //如果已經(jīng)松開,就立刻調(diào)用SingleTapConfirmed
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        //如果處理Message的時候還沒松開,就設(shè)置mDeferConfirmSingleTap為true,在UP事件的時候調(diào)用SingleTapConfirme
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

之前看過,TAP消息是延時(300ms)發(fā)送的,然而實際邏輯中,是抬起手指才算是點擊,所以這里處理TAP的時候就不一定立刻調(diào)用onSingleTapConfirmed(),而是判斷手指是否松開了,是松開的話就立刻回調(diào)。如果還未松開那就把標志位mDeferConfirmSingleTap設(shè)置為true,等到收到UP事件的時候再回調(diào)。

  • onFling()回調(diào):當UP事件的速度大于一定速度時,就會回調(diào)onFling(),至于mIgnoreNextUpEvent參數(shù),是只有鼠標右鍵點擊的時候才會為true,具體看后面。

2.4. 多點觸摸的處理

對于多個手指落下,前面的統(tǒng)一處理已經(jīng)是把所有手指的坐標值加起來然后算平均值了,所以我們多根手指觸摸的時候,其實滑動的實際點是這些手指的中心焦點?;乜瓷厦娴腗OVE事件處理,中心焦點的位置值FocusX和FocusY決定onScroll(onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY))的后兩個參數(shù)值,所以處理多點觸控的話就使用它們比較方便。因為MotionEvent是使用數(shù)組裝著當前屏幕上所有指針的動作的,使用前兩個參數(shù)的話還要循環(huán)用getX(int pointerIndex)getY(int pointerIndex)方法取出各個指針的值再自己處理。

    //...
        case MotionEvent.ACTION_POINTER_DOWN:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            // Cancel long press and taps
            //如果有多根手指按下,取消長按和點擊計時
            cancelTaps();
            break;

        case MotionEvent.ACTION_POINTER_UP:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;

            // Check the dot product of current velocities.
            // If the pointer that left was opposing another velocity vector, clear.
            //計算每一秒鐘的滑動像素
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final int upIndex = ev.getActionIndex();
            final int id1 = ev.getPointerId(upIndex);
            final float x1 = mVelocityTracker.getXVelocity(id1);
            final float y1 = mVelocityTracker.getYVelocity(id1);
            //如果剩下的手指速度方向是和抬起那根手指的速度相反方向的,就說明不是fling,清空速度監(jiān)聽
            for (int i = 0; i < count; i++) {
                if (i == upIndex) continue;

                final int id2 = ev.getPointerId(i);
                final float x = x1 * mVelocityTracker.getXVelocity(id2);
                final float y = y1 * mVelocityTracker.getYVelocity(id2);

                final float dot = x + y;
                if (dot < 0) {
                    mVelocityTracker.clear();
                    break;
                }
            }
            break;
    //...

    private void cancelTaps() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        mInLongPress = false;
        mInContextClick = false;
        mIgnoreNextUpEvent = false;
    }

這是對多個手指的UP和DOWN事件處理,其實就是做一些取消操作而讓多點觸摸不影響單點觸摸的應(yīng)用,例如在多個手指落下的時候取消點擊信息等。

2.5. ContextClick的處理

ContextClick的事件是由onGenericMotionEvent()傳入:

    public boolean onGenericMotionEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
        }

        final int actionButton = ev.getActionButton();
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_BUTTON_PRESS:
                //按下觸控筆首選按鈕或者鼠標右鍵
                if (mContextClickListener != null && !mInContextClick && !mInLongPress
                        && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
                        || actionButton == MotionEvent.BUTTON_SECONDARY)) {
                    if (mContextClickListener.onContextClick(ev)) {
                        mInContextClick = true;
                        mHandler.removeMessages(LONG_PRESS);
                        mHandler.removeMessages(TAP);
                        return true;
                    }
                }
                break;

            case MotionEvent.ACTION_BUTTON_RELEASE:
                if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
                        || actionButton == MotionEvent.BUTTON_SECONDARY)) {
                    mInContextClick = false;
                    //無視下一個UP事件,因為它是由鼠標右鍵或者觸控筆鍵帶起的
                    mIgnoreNextUpEvent = true;
                }
                break;
        }
        return false;
    }

由此可以,當按鍵按下(ACTION_BUTTON_PRESS)的時候已經(jīng)回調(diào)onContextClick()。

總結(jié)

讀完源碼,總結(jié)出來的每個回調(diào)的調(diào)用時機如下表:

PS:除去onContextClick(),因為它的按下鼠標右鍵時候是發(fā)出一系列的事件。

回調(diào)/輸入事件 DOWN事件 MOVE事件 UP事件
onDown(MotionEvent e) × ×
onShowPress(MotionEvent e) × ×
onLongPress(MotionEvent e) × ×
onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY) × ×
onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY)) × ×
onSingleTapUp(MotionEvent e) × ×
onSingleTapConfirmed(MotionEvent e) × ×
onDoubleTap(MotionEvent e) × ×
onDoubleTapEvent(MotionEvent e)

從上面的分析可以看出,雖然GestureDetector能識別很多手勢,但是也是不能滿足所有的需求的,如滑動和長按之后松開沒有回調(diào)(這個可以重寫onTouch()捕捉UP事件實現(xiàn))、多點觸控縮放手勢的實現(xiàn)(這個可以用ScaleGestureDetector)等。

后話

有人問我看GestureDetector源碼這么仔細有什么用,它又不是很常用的東西,網(wǎng)上隨便一搜一堆資料。我的回答是因為我覺得要用一個東西的話,首先就是要搞清楚它能干什么,它的限制是什么,為什么要選擇它,關(guān)于這些方面,網(wǎng)上的很多關(guān)于GestureDetector的資料都沒有達到我想了解的程度,加上GestureDetector并不復(fù)雜,所以寫下了這篇博客,這樣就可以從源碼層面上了解到它的回調(diào)是什么時候調(diào)用,有bug的時候也能更快的找出。
  不管怎樣,GestureDetector里面的SimpleOnGestureListener的設(shè)計,和對觸摸事件的處理方式是很值得我學(xué)習(xí)的,記錄分享至此,水平有限,如果錯漏,歡迎指正和討論。

最后編輯于
?著作權(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)容