Android手勢(shì)檢測(cè)——GestureDetector全面分析

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

前言

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

GestureDetector介紹

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


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

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

1.OnGestureListener,這個(gè)Listener監(jiān)聽一些手勢(shì),如單擊、滑動(dòng)、長(zhǎng)按等操作:

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

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

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

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

  • onContextClick(MotionEvent e):當(dāng)鼠標(biāo)/觸摸板,右鍵點(diǎn)擊時(shí)候的回調(diào)。

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

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

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

GestureDetector的使用

GestureDetector的使用很簡(jiǎn)單,因?yàn)樗墓δ芫褪嵌x為識(shí)別手勢(shì),所以使用的話就是輸入完整的觸摸事件(完整的意思就是用戶所有的觸摸操作都是輸入給它。為什么要強(qiáng)調(diào)完整,因?yàn)槲?a href="http://www.lxweimin.com/p/9f98dda95ce7" target="_blank">上一篇博客就是分享如何攔截子View的部分觸摸事件),識(shí)別然后進(jìn)行相應(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)聽鼠標(biāo)右鍵點(diǎn)擊事件
                return mGestureDetector.onGenericMotionEvent(event);
            }
        });
    }

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

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

GestureDetector源碼分析

1. 初始化處理

GestureDetector的源碼接近800行,這在Android源碼中已經(jīng)算是比較短的了(畢竟注釋也占一兩百行了),所以說它的實(shí)現(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) {
            //相當(dāng)于下面的getScaledTouchSlop,表示滑動(dòng)的時(shí)候,手的移動(dòng)要大于這個(gè)距離才開始移動(dòng)控件
            touchSlop = ViewConfiguration.getTouchSlop();
            //相當(dāng)于下面的getScaledDoubleTapTouchSlop,表示點(diǎn)擊的時(shí)候,手指移動(dòng)大于這個(gè)距離,就被認(rèn)為不可能是雙擊
            doubleTapTouchSlop = touchSlop; 
            //相當(dāng)于下面的getScaledDoubleTapSlop,表示第二次點(diǎn)擊的時(shí)候,和第一次的點(diǎn)擊點(diǎn)位置距離如果大于這個(gè),就被認(rèn)為不是雙擊
            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();
        }
        //做平方好計(jì)算距離,后面的距離對(duì)比也是用平方
        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

可見GestureDetector的創(chuàng)建就是初始化一些屬性,然后就是把對(duì)應(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í)候還沒松開,就設(shè)置mDeferConfirmSingleTap為true,在UP事件的時(shí)候調(diào)用SingleTapConfirme
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

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

    //長(zhǎng)按處理
    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();

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


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

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

        // Determine focal point
        // 是非主要指針抬起動(dòng)作的話會(huì)跳過
        float sumX = 0, sumY = 0;
        final int count = ev.getPointerCount();
        //把所有還在觸摸的手指的位置x,y加起來,后面求平均數(shù),算出中心焦點(diǎn)
        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;
        }

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

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

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

2.1. DOWN事件處理

下面進(jìn)入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
                    //延時(shí)發(fā)出單擊事件,如果到了時(shí)間(300ms)還沒有取消的話就確認(rèn)是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;
            //處理長(zhǎng)按
            if (mIsLongpressEnabled) {
                mHandler.removeMessages(LONG_PRESS);
                //延時(shí)發(fā)送長(zhǎng)按事件
                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
            }
            //延時(shí)發(fā)送showPress事件
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            handled |= mListener.onDown(ev);
            break;
    //...

    //判斷第二次點(diǎn)擊是否有效雙擊
    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
            MotionEvent secondDown) {
        //第一次點(diǎn)擊后是否有移動(dòng)超出范圍
        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();
        //判斷第二次點(diǎn)擊是否在附近,在附近才被認(rèn)為是雙擊
        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    }

可見,對(duì)DOWN事件涉及:

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

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

2.2. MOVE事件處理

然后再看看對(duì)MOVE事件的處理:

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

可見,對(duì)MOVE事件涉及:

  • onDoubleTapEvent()回調(diào):只要確認(rèn)是雙擊之后,mIsDoubleTapping為true,除了長(zhǎng)按,后面的MOVE事件都會(huì)只回調(diào)onDoubleTapEvent()
  • onScroll()回調(diào):當(dāng)MOVE不是長(zhǎng)按,不是DoubleTapEvent之后,當(dāng)移動(dòng)距離大于一定距離之后,就會(huì)進(jìn)入Scroll模式,然后兩個(gè)MOVE事件的位移距離scrollX或者scrollY大于1px,都會(huì)調(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) {
                //長(zhǎng)按結(jié)束
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
                handled = mListener.onSingleTapUp(ev);
                //處理單擊確認(rèn),具體邏輯看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;
    //...

可見,對(duì)MOVE事件涉及:

  • onDoubleTapEvent()回調(diào):只要確認(rèn)是雙擊之后,mIsDoubleTapping為true,除了長(zhǎng)按,后面的MOVE事件都會(huì)只回調(diào)onDoubleTapEvent()
  • onSingleTapUp()回調(diào):DOWN事件之后沒有MOVE,或者M(jìn)OVE的距離沒有超出范圍,mAlwaysInTapRegion才不會(huì)變成false,回調(diào)onSingleTapUp()
  • onSingleTapConfirmed()回調(diào):從前面GestureHandler里面的TAP消息的實(shí)現(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í)候還沒松開,就設(shè)置mDeferConfirmSingleTap為true,在UP事件的時(shí)候調(diào)用SingleTapConfirme
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

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

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

2.4. 多點(diǎn)觸摸的處理

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

    //...
        case MotionEvent.ACTION_POINTER_DOWN:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            // Cancel long press and taps
            //如果有多根手指按下,取消長(zhǎng)按和點(diǎn)擊計(jì)時(shí)
            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.
            //計(jì)算每一秒鐘的滑動(dòng)像素
            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;
    }

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

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:
                //按下觸控筆首選按鈕或者鼠標(biāo)右鍵
                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;
                    //無視下一個(gè)UP事件,因?yàn)樗怯墒髽?biāo)右鍵或者觸控筆鍵帶起的
                    mIgnoreNextUpEvent = true;
                }
                break;
        }
        return false;
    }

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

總結(jié)

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

PS:除去onContextClick(),因?yàn)樗陌聪率髽?biāo)右鍵時(shí)候是發(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能識(shí)別很多手勢(shì),但是也是不能滿足所有的需求的,如滑動(dòng)和長(zhǎng)按之后松開沒有回調(diào)(這個(gè)可以重寫onTouch()捕捉UP事件實(shí)現(xiàn))、多點(diǎn)觸控縮放手勢(shì)的實(shí)現(xiàn)(這個(gè)可以用ScaleGestureDetector)等。

后話

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

最后編輯于
?著作權(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閱讀 230,501評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,371評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評(píng)論 2 380

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