安卓源码GestureDetector类解读

前言

安卓源码给我们提供了一个GestureDetector类,来监听手势,点击,长按,双击,滚动,抛等。本文通过解读Gesture类来看看安卓源码是怎样判断各类手势的?搞清楚了这个,我们就可以自定义手势啦,比如一张图片,点击返回,长按保存,单个手指双击 图片放大到2倍或者由放大状态变回原来的大小,两个手指捏 缩小图片,两个手指张开 放大图片等等。

下面我们就来看GestureDetector类的具体实现。涉及到fling抛的代码会放在后面讲,本文会略过。

1. GestureDetector类中的接口

GestureDetector类提供了两个接口OnGestureListenerOnDoubleTapListenerOnGestureListener用来监听单个手指事件,OnDoubleTapListener用来监听两个手指事件。具体方法如下:

接口OnGestureListener

public interface OnGestureListener {
        //手指按下就会触发(调用onTouch()方法的ACTION_DOWN事件时触发)
        boolean onDown(MotionEvent e);
        //一次点击up事件;在touch down后又没有滑动(onScroll),又没有长按(onLongPress),然后Touchup时触发
        boolean onSingleTapUp(MotionEvent e);
        /*
        down事件发生而move或则up还没发生前触发该
事件;Touch了还没有滑动时触发(与onDown,onLongPress)比较onDown只要Touch down一定立刻触发。而Touchdown后过一会没有滑动先触发onShowPress再是onLongPress。所以Touchdown后一直不滑动
        onLongPress之前触发
        */
        void onShowPress(MotionEvent e);
        //长按,触摸屏按下后既不抬起也不移动,过一段时间后触发
        void onLongPress(MotionEvent e);
        //滚动,触摸屏按下后移动(执行onTouch()方法的ACTION_MOVE事件时会触发)
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        //滑动,触摸屏按下后快速移动并抬起,会先触发滚动手势,跟着触发一个滑动手势
        //在ACTION_UP时才会触发
        /*参数:
            e1:第1个ACTION_DOWN MotionEvent 并且只有一个;
            e2:最后一个ACTION_MOVE MotionEvent ;
            velocityX:X轴上的移动速度,像素/秒 ;
            velocityY:Y轴上的移动速度,像素/秒.
          触发条件:X轴的坐标位移大于FLING_MIN_DISTANCE,且移动速度大于FLING_MIN_VELOCITY个像素/秒
         */       
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

接口OnDoubleTapListener

public interface OnDoubleTapListener {
        /* 
        1. 单击确认,用来判定该次点击是SingleTap而不是DoubleTap,如果连续点击两次就是DoubleTap手势,如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为SingleTap而不是DoubleTap,然后触发SingleTapConfirmed事件。
        2. 不同于onSingleTapUp,他是在GestureDetector确信用户在第一次触摸屏幕后,没有紧跟着第二次触摸屏幕,也就是不是“双击”的时候触发
        */
        boolean onSingleTapConfirmed(MotionEvent e);
        //在双击的第二下,Touch down时触发 。
        boolean onDoubleTap(MotionEvent e);
        //双击的第二下Touch down和up都会触发,可用e.getAction()区分。
        boolean onDoubleTapEvent(MotionEvent e);
    }

各种手势调用方法如下:

  • 快速点击(没有滑动):onDown() -> onSingleTapUp() -> onSingleTapConfirmed()
  • 手指一直按下,即长按(没有滑动,没有抬起):onDown() -> onShowPress() -> onLongPress()
  • 滚动:onDown() -> onScroll() -> onScroll() ... ...
  • 抛:onDown() -> onScroll() -> ... ... -> onFling()

(onTouch()::ACTION_DOWN) -> onDown() -> (onTouch()::ACTION_MOVE) -> onScroll() -> ... ...(onTouch()::ACTION_MOVE) ->onScroll() -> (onTouch()::ACTION_UP) -> onFling()

2. GestureDetector类中的内部类SimpleOnGestureListener

内部类SimpleOnGestureListener,实现了上面2个接口:

public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        public void onLongPress(MotionEvent e) {
        }
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            return false;
        }
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            return false;
        }
        public void onShowPress(MotionEvent e) {
        }
        public boolean onDown(MotionEvent e) {
            return false;
        }
        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }
        public boolean onDoubleTapEvent(MotionEvent e) {
            return false;
        }
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return false;
        }
    }

3. 消息类GestureHandler

GestureDetector类中的消息类型有3种:TAP,SHOW_PRESS,LONG_PRESS,

  • SHOW_PRESS:回调onShowPress()接口

  • LONG_PRESS:回调onLongPress()接口,即长按事件。

  • TAP:如果当前手指不是down状态,回调onSingleTapConfirmed()接口,即单击事件。

具体实现如下:

private class GestureHandler extends Handler {
        GestureHandler() {
            super();
        }
        GestureHandler(Handler handler) {
            super(handler.getLooper());
        }
        @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
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;
            default:
                throw new RuntimeException("Unknown message " + msg); //never
            }
        }
    }
    //处理长按事件
    private void dispatchLongPress() {
        mHandler.removeMessages(TAP);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }

4. 单击,长按,双击,移动等这些事件的判断条件

源码是怎么判断单击,长按,双击,移动等这些事件的呢?当手指处于按下状态,即down事件,会发送一些消息TAP,LONG_PRESS,SHOW_PRESS(延时发送或在指定的时间点发送),然后在接下来的事件中分别去判断,如果是移动事件,移除TAP,LONG_PRESS和SHOW_PRESS消息;如果是长按事件,就移除TAP消息。这样,如果没有移除TAP消息,那么handler中会回调相应接口。一般双击的判断在down事件,移动的判断发生在move事件,而双击的消费发生在up事件,回调onShowPress(),长按回调onLongPress(),单击回调onSingleTapConfirmed()都发生在handleMessage中。

4.1 单击

点击时间在 DOUBLE_TAP_TIMEOUT 内未进行第二次单击事件。

单击的回调接口为onSingleTapConfirmed()onSingleTapConfirmed()的调用有两个地方:

  1. handlerMessage中
  2. up事件中

handlerMessage中消息类型为TAP并且只要当前不是按下状态就会调用,up事件中,首先判断双击和长按是否正在进行,如果都不是,判断如果移动区域在mTouchSlopSquare之内并且mDeferConfirmSingleTap为true,会调用。

4.2 长按

  1. 手指处于按下状态
  2. 按下时间未超过LONGPRESS_TIMEOUT。通过ViewConfiguration.getLongPressTimeout()来获取
  3. 移动未超过mTouchSlopSquare的距离。

4.3 双击

  1. 是否有上一次的单击事件(上一次down事件和up事件不为空)

  2. mAlwaysInBiggerTapRegion为true。即每次单击的移动距离在 mDoubleTapTouchSlopSquare 之内。

  3. 第二次单击的down事件和第一次单击的up事件的时间间隔在 DOUBLE_TAP_MIN_TIME 和 DOUBLE_TAP_TIMEOUT之间。

  4. 第一次按下的位置和第二次按下的位置之间的距离小于mDoubleTapSlopSquare。

源码如下:

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;
                    }

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);
    }

4.4 移动

mAlwaysInTapRegion为false 或者 mAlwaysInTapRegion为true并且 移动区域在 mTouchSlopSquare 和 mDoubleTapTouchSlopSquare 之间。

5. 各个事件的具体实现和分析

看源码之前,我们先来看一些变量。

   private boolean mStillDown;//是否按下。ACTION_DOWN为true,ACTION_UP 和 cancle 为false
   
   //是否延迟确定点击。确定点击即消费点击,其实就是点击事件。在handleMessage中消息类型为TAP时,如果手指为非按下状态,则回调onSingleTapConfirmed接口,否则,mDeferConfirmSingleTap置为true,其他情况全部置为false:ACTION_DOWN,ACTION_UP结尾,处理长按事件,cancle。
   private boolean mDeferConfirmSingleTap;
   
   private boolean mInLongPress;//是否正在处理长按事件
   
   //是否允许双击事件发生,默认为true,除非手动去设置。只有为true,双击事件才能进行
   private boolean mIsLongpressEnabled;
   private boolean mIsDoubleTapping;//当前事件是否是双击事件
   
   //是否当前手指仅在小范围内移动,当手指仅在小范围内移动时,视为手指未曾移动过,不会触发onScroll手势。用来判断点击事件和移动事件。移动距离大于mTouchSlopSquare时,为false。
   private boolean mAlwaysInTapRegion;
   
   //是否当前手指在较大范围内移动,判断双击手势。此值为true时,双击手势成立。移动距离大于mDoubleTapTouchSlopSquare时,为false。
   private boolean mAlwaysInBiggerTapRegion;
   

关于点击区域的变量:

// 判断是否是单击,是否能构成onScroll事件。移动距离小于mTouchSlopSquare,则是单击,大于,则是onScroll
private int mTouchSlopSquare;

//判断是否构成双击,单次单击移动距离小于mDoubleTapTouchSlopSquare,mAlwaysInBiggerTapRegion为true。
private int mDoubleTapTouchSlopSquare;

//判断是否构成双击,第二次按下的位置和第一次抬起的位置的距离小于mDoubleTapSlopSquare,则构成双击
private int mDoubleTapSlopSquare;

关于时间的变量:

    //发送SHOW_PRESS消息的时间,发生在ACTION_MOVE和ACTION_UP之前。如果在ACTION_MOVE中判断为onScroll事件,则    //取消SHOW_PRESS消息。
    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
    
    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
    
    //判断双击
    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
    private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
  • 发送长按LONG_PRESS消息的延时为:

mCurrentDownEvent.getDownTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT

  • 发送SHOW_PRESS消息的时间:

mCurrentDownEvent.getDownTime() + TAP_TIMEOUT

  • 发送TAP消息的延时:

DOUBLE_TAP_TIMEOUT

    //上一次焦点的x坐标
    private float mLastFocusX;
    private float mLastFocusY;
    //当前焦点的x坐标
    private float mDownFocusX;
    private float mDownFocusY;
    
    private MotionEvent mCurrentDownEvent;//当前down事件
    private MotionEvent mPreviousUpEvent;//上一次up事件

5.1 MotionEvent.ACTION_POINTER_DOWN

case MotionEvent.ACTION_POINTER_DOWN:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            // Cancel long press and taps
            cancelTaps();
            break;
            
private void cancelTaps() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        if (mInLongPress) {
            mInLongPress = false;
        }
    }

5.2 MotionEvent.ACTION_POINTER_UP

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

5.3 MotionEvent.ACTION_DOWN

  1. 如果当前handler有TAP消息,移除所有的TAP消息

  2. 判断是否是双击,如果是,设置回调onDoubleTap和onDoubleTapEvent。如果不是,发送一个延时消息TAP,延时为DOUBLE_TAP_TIMEOUT。

  3. 如果允许长安事件发生,在 mCurrentDownEvent.getDownTime()+TAP_TIMEOUT+LONGPRESS_TIMEOUT 时间发送LONG_PRESS消息。

  4. 发送SHOW_PRESS消息,在mCurrentDownEvent.getDownTime() + TAP_TIMEOUT 时间。

源码如下:

        case MotionEvent.ACTION_DOWN:
            if (mDoubleTapListener != null) {
                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
                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                    // Give a callback with down event of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else {
                    // This is a first tap
                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                }
            }
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            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);
                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
            }
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            handled |= mListener.onDown(ev);
            break;

5.4 MotionEvent.ACTION_MOVE

判断是否正在进行长按事件,是,break,否,向下执行。

判断是否是双击事件,是,回调onDoubleTapEvent()接口,否,向下执行。

判断是否是scroll事件,是,移除所有消息,并且向下执行。

移动距离只否满足双击的条件

       case MotionEvent.ACTION_MOVE:
            if (mInLongPress) {
                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) {
                final int deltaX = (int) (focusX - mDownFocusX);
                final int deltaY = (int) (focusY - mDownFocusY);
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                if (distance > mTouchSlopSquare) {
                    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)) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
            }
            break;

5.5 MotionEvent.ACTION_UP

  1. 如果是双击事件,回调onDoubleTapEvent接口

  2. 如果正在执行长按事件,移除TAP消息,mInLongPress置为false。mInLongPress为boolean类型的变量,判断是否正在执行长按事件。

  3. 如果是移动事件,回调onSingleTapUp接口;

  4. 移除SHOW_PRESS和LONG_PRESS消息。

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) {
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion) {
                handled = mListener.onSingleTapUp(ev);
                if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                    mDoubleTapListener.onSingleTapConfirmed(ev);
                }
            } else {
                // 这里是判断fling事件的代码,这里暂不讨论
            }
            if (mPreviousUpEvent != null) {
                mPreviousUpEvent.recycle();
            }
            // Hold the event we obtained above - listeners may have changed the original.
            mPreviousUpEvent = currentUpEvent;
            
            mIsDoubleTapping = false;
            mDeferConfirmSingleTap = false;
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
            break;

5.6 MotionEvent.ACTION_CANCEL

case MotionEvent.ACTION_CANCEL:
            cancel();
            break;
        }
        
private void cancel() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        mIsDoubleTapping = false;
        mStillDown = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        if (mInLongPress) {
            mInLongPress = false;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容