Android开发:善于将系统源码为己用,会“抄”也是技能!

作者:小缘

起源于一位同事问我:“怎么优雅的监听双击”这个行为?

其实很多类似的事件相关的,我们都可以参考系统源码,因为有时候完全引入系统能力有些麻烦,我们可能就想顺手实现一个功能。

第一次看我文章的小伙伴可以关注一下我,每天更新各种技术干货,分享更多最热程序员圈内事。

例如上面同事问的:

怎么优雅的监听双击?

相信大家或多或少都有一些实现方案,不过系统有给我们提供GestureDetector类,如果你熟知该类实现,那么就能选择出于系统一样的方案,代码的认可度也会提高。

所以今天我们就借机学习下:

GestureDetector关于支持的手势是如何检测的?

下面内容是小缘分析的。

我们在创建这个类实例的时候,需要把接口OnGestureListener(用来监听各种手势)实现并作为参数传进它的构造方法中:

GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }
});

解释一下各个方法的回调时机(都非常容易理解):

  • onDown:手指按下;
  • onShowPress:手指按下后,100毫秒内未抬起、未移动;
  • onSingleTapUp:手指按下后未移动,并在500毫秒内抬起(可以认定为单击);
  • onScroll:手指拖动;
  • onLongPress:长按(手指按下后,500毫秒内未抬起、未移动);
  • onFling:手指快速拖动后松手(惯性滚动);

除了OnGestureListener之外,还有一个OnDoubleTapListener,看名字就能猜到是用来监听双击事件的了:

gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }
});

解释一下:

  • onSingleTapConfirmed:已经确认这是一次单击事件,想触发双击必须继续快速点击两次屏幕(即:手指抬起之后,300毫秒内没等到手指再次按下);
  • onDoubleTap:触发双击事件(手指抬起后300毫秒内再次按下(注意:是再次按下时就触发,并不是等它抬起后才触发))
  • onDoubleTapEvent:触发双击后的手指触摸事件,包括ACTION_DOWN、ACTION_MOVE、ACTION_UP(注意:在触发长按后,不会继续收到ACTION_MOVE事件,因为在手指长按过程中,是不需要处理手指移动的动作的,也就是会直接忽略ACTION_MOVE的事件。还有,此方法回调后,在触发长按事件之前,如有新手指按下,则不再认定是双击了,所以不会继续回调此方法,取而代之的是onScroll)。此方法与上面的onDoubleTap方法的区别就是,onDoubleTap在一次双击事件中只会回调一次,而这个方法能回调多次;

好,对它有个初步了解之后,来看看它是怎么检测这些事件的。

一、onDown

这个类的代码很少,不到800行(SDK28)。

首先来看onDown方法是在什么时候回调的 (可在刚刚的接口方法中 CTRL + Click 对应的方法名来定位到具体调用的位置) :

public boolean onTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        case MotionEvent.ACTION_DOWN:
            ......
            handled |= mListener.onDown(ev);
            break;
        ......
    }
    ......
    return handled;
}

超级简单,监听到ACTION_DOWN事件就立即回调了。

二、onShowPress

接着来看看onShowPress方法(用刚刚说的方法来定位):

private class GestureHandler extends Handler {

    ......

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SHOW_PRESS:
                mListener.onShowPress(mCurrentDownEvent);
                break;
            ......
        }
    }
}

它会在GestureHandler收到what为SHOW_PRESS的消息后回调,看看在哪里发的这个消息:

private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();

public boolean onTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        case MotionEvent.ACTION_DOWN:
            ......
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            ......
            break;
        ......
    }
    ......
    return handled;
}

emmm,同样在收到ACTION_DOWN后,会向mHandler(也就是GestureHandler)发送一个指定时间的消息,而这个时间就是事件按下的时间加上TAP_TIMEOUT的时长,可以看到TAP_TIMEOUT的值是根据ViewConfiguration的getTapTimeout方法来获取的,点开一看:

private static final int TAP_TIMEOUT = 100;

public static int getTapTimeout() {
    return TAP_TIMEOUT;
}

是100(ms),也就是说,当手指按下后,如果这个延时任务100毫秒内没有被取消,那么onShowPress方法就会回调。

三、onSingleTapUp & onScroll

好,现在来看看onSingleTapUp:

public boolean onTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        case MotionEvent.ACTION_UP:
            ......
            if (mIsDoubleTapping) {
                ......
            } else if (mInLongPress) {
                ......
            } else if (mAlwaysInTapRegion) {
                handled = mListener.onSingleTapUp(ev);
                ......
            }
            ......
            break;
        ......
    }
    ......
    return handled;
}

可以看到,它是在ACTION_UP的时候回调的,回调需满足三个条件,分别是:

  1. mIsDoubleTapping为false(即双击事件未触发);
  2. mInLongPress为false(即长按事件未触发);
  3. mAlwaysInTapRegion为true;

mAlwaysInTapRegion什么时候为true,什么时候为false呢:

private void cancelTaps() {
    ......
    mAlwaysInTapRegion = false;
    ......
}

public boolean onTouchEvent(MotionEvent ev) {
    ......

    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            ......
            cancelTaps();
            break;
        ......
        case MotionEvent.ACTION_DOWN:
            ......
            mAlwaysInTapRegion = true;
            ......
            break;
        case MotionEvent.ACTION_MOVE:
            ......
             if (mIsDoubleTapping) {
                ......      
             }else if (mAlwaysInTapRegion) {
                final int deltaX = (int) (focusX - mDownFocusX);
                final int deltaY = (int) (focusY - mDownFocusY);
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                int slopSquare = mTouchSlopSquare;
                if (distance > slopSquare) {
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mAlwaysInTapRegion = false;
                    ......
                }
                ......
            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                ......
            }
            ......
            break;
        ......
    }
    ......
    return handled;
}

一共有三处赋值的地方,分别是:

  1. ACTION_POINTER_DOWN(另一只手指按下)时为false,也就是说,如果第一只手指按下后,100毫秒内有新的手指按下,那么当手指抬起时不会触发onSingleTapUp;
  2. ACTION_DOWN(第一只手指按下)时为true;
  3. ACTION_MOVE时,看else if里面的那个if, 它是判断distance(手指的移动距离)是否大于slopSquare(触发移动的最小距离),如果是的话,会回调onScroll方法,并把mAlwaysInTapRegion设为false,这就说明,如果手指按下100秒内开始了拖动的话,那么onSingleTapUp方法也是不会回调的;

还可以看到当mAlwaysInTapRegion被设为false之后,下一次的ACTION_MOVE到来时,如果没有触发双击(即上面的mIsDoubleTapping为false)并且手指的水平或垂直移动距离不为0的话,就会一直回调onScroll方法。

好,现在onScroll也讲了,轮到onLongPress了。

四、onLongPress

private class GestureHandler extends Handler {

    ......

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SHOW_PRESS:
                mListener.onShowPress(mCurrentDownEvent);
                break;

             case LONG_PRESS:
                 dispatchLongPress();
                 break;

            ......
        }
    }
}

private void dispatchLongPress() {
    mInLongPress = true;
    mListener.onLongPress(mCurrentDownEvent);
}

跟onShowPress方法一样也是借助Handler的定时消息机制来实现的,它在收到LONG_PRESS的消息之后,会调用dispatchLongPress方法,dispatchLongPress方法首先会标记mInLongPress为true

(注意:这将会影响到上面说到的onSingleTapUp的回调,因为onSingleTapUp的回调条件是需要mInLongPress为false的(即未触发长按事件))

接着就回调onLongPress方法。

那么LONG_PRESS消息在什么时候发送的呢?

也是在ACTION_DOWN的时候 :

public boolean onTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        case MotionEvent.ACTION_DOWN:
            ......
             if (mIsLongpressEnabled) {
                    mHandler.removeMessages(LONG_PRESS);
                    mHandler.sendMessageAtTime(
                            mHandler.obtainMessage(...),
                            mCurrentDownEvent.getDownTime() + ViewConfiguration.getLongPressTimeout());
              }
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            ......
            break;
        ......
    }
    ......
    return handled;
}

在发送消息之前,会先检查是否开启了监听长按事件,还有取消上一次发出且未执行的长按回调任务。

可以看到定的时间为事件按下时间加上getLongPressTimeout方法返回的时长,默认是500(ms),也就是当手指按下半秒后,onLongPress方法就会被回调,当然了,前提是这个任务没有被取消。

有以下几种情况会导致长按回调任务被取消:

  1. 500ms内有新手指按下;
  2. 500ms内触发了onScroll,即手指移动超过指定距离;
  3. 500ms内手指抬起;
  4. 500ms内收到了ACTION_CANCEL事件(该ACTION一般源自父容器的私自创建);

来看看onFling:

五、onFling

public class ViewConfiguration {
   private static final int MINIMUM_FLING_VELOCITY = 50;

   public static int getMinimumFlingVelocity() {
        return MINIMUM_FLING_VELOCITY;
    }
    ......
}

private int mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();

public boolean onTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        case MotionEvent.ACTION_UP:
            ......
            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);
            }
            ......
            break;
        ......
    }
    ......
    return handled;
}

跟我们平时处理惯性滚动没什么区别,只是它在回调之前会先判断滑动的速度 是否大于 指定的最小速度,否则不进行滚行滚动。

好,最后我们来看一下onSingleTapConfirmed、onDoubleTap、onDoubleTapEvent分别是怎么处理的:

六、onSingleTapConfirmed、onDoubleTap、onDoubleTapEvent

private class GestureHandler extends Handler {

    ......

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            ......
            case TAP:
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;
            ......
        }
    }
}

public boolean onTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;

    switch (action & MotionEvent.ACTION_MASK) {
        ......
        case MotionEvent.ACTION_DOWN:
            ......
            if (isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                mIsDoubleTapping = true;
                handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else {
                mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
            }
            mStillDown = true;
            ......
            break;

        case MotionEvent.ACTION_MOVE:
            if (mIsDoubleTapping) {
                  handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            }
            ......
            break;

        case MotionEvent.ACTION_UP:
            mStillDown = false;
            if (mIsDoubleTapping) {
                  handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            }
            ......
            if (mIsDoubleTapping) {
                ......
            } else if (mInLongPress) {
                ......
            } else if (mAlwaysInTapRegion) {
                handled = mListener.onSingleTapUp(ev);
                if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                        mDoubleTapListener.onSingleTapConfirmed(ev);
                }
            }
            ......
            break;
        ......
    }
    ......
    return handled;
}

首先看onSingleTapConfirmed方法,它在两个地方有调用,分别是:

  • GestureHandler收到TAP消息时
  • 处理ACTION_UP事件时

当GestureHandler收到TAP消息时它会先检查手指是否已经抬起(!mStillDown),如果已经抬起了的话,就会立即调用,否则把mDeferConfirmSingleTap标记为true,表示onSingleTapConfirmed方法应在ACTION_UP时回调,可以看到在处理ACTION_UP时,如果手指没有移动过并且没触发长按的话,就会判断mDeferConfirmSingleTap是否为true,是的话,就会回调onSingleTapConfirmed方法。

接着看ACTION_DOWN,它会调用isConsideredDoubleTap方法来判断此次事件是否被认定是双击,如果不是,就会向GestureHandler发一条延时消息(延时回调onSingleTapConfirmed方法)

如果是的话,就会把mIsDoubleTapping标记为true,然后依次回调onDoubleTap和onDoubleTapEvent。可以看到在ACTION_MOVE和ACTION_UP中也会根据mIsDoubleTapping来判断是否继续回调onDoubleTapEvent方法。

好,那现在来看一下,它究竟是怎么认定为双击的,看看isConsideredDoubleTap方法:

private int mDoubleTapSlopSquare;

private void init(Context context) {
    ......
    int doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
    ......
    mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
}

private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
        MotionEvent secondDown) {
    if (!mAlwaysInBiggerTapRegion) {
        return false;
    }

    final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
    // DOUBLE_TAP_TIMEOUT = 300,  DOUBLE_TAP_MIN_TIME = 40
    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();

    int slopSquare = mDoubleTapSlopSquare;
    return (deltaX * deltaX + deltaY * deltaY < slopSquare);
}

先是判断了mAlwaysInBiggerTapRegion,如果它为false的话,则代表被其他动作(ACTION_MOVE、ACTION_CANCEL)中断了双击事件的检测,所以直接返回false(即不认定是双击)了。

接着会判断第二次按下与第一次按下的时间间隔,如果大于300毫秒则认定是超时,如果小于40毫秒也会忽略(太快了)。

最后,可能很多同学咋一看,看不出来是什么逻辑,仔细看几次,就会知道这其实是在计算第一次按下和第二次按下的坐标间隔距离,用的就是计算两点间距离的公式(√(x1 - x2)² + (y1 - y2)²)。

这时有同学可能会问:

不是还要开平方吗?怎么它代码里没有呢?

其实,那个slopSquare(也就是能够被认定为双击的最大间隔)在初始化时,就已经作了平方运算了,所以这里就不需要开平方了。

emmm,那么isConsideredDoubleTap方法最后一句的意思就是,判断两次触摸事件的坐标间隔是否在指定的最大间隔范围内,如果是的话,则认定是双击。

扩展:GestureDetector是如何支持多指触控的呢?

大家可以评论区留下你的看法


喜欢本文的话,不妨给我点个小赞、评论区留言或者转发支持一下呗~

最后

不知不觉自己已经做了几年开发了,由记得刚出来工作的时候感觉自己能牛逼,现在回想起来感觉好无知。懂的越多的时候你才会发现懂的越少。

对于程序员来说,要学习的知识内容、技术有太多太多。

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,可以关注我,每天更新各种技术干货,分享更多最热程序员圈内事.

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