android view gesturedetector,每日一问 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在一次双击事件中只会回调一次,而这个方法能回调多次;

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

源码分析

这个类的代码很少,不到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方法(用刚刚说的方法来定位):

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:

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的时候回调的,回调需满足三个条件,分别是:

mIsDoubleTapping为false(即双击事件未触发);

mInLongPress为false(即长按事件未触发);

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;

}

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

ACTION_POINTER_DOWN(另一只手指按下)时为false,也就是说,如果第一只手指按下后,100毫秒内有新的手指按下,那么当手指抬起时不会触发onSingleTapUp;

ACTION_DOWN(第一只手指按下)时为true;

ACTION_MOVE时,看else if里面的那个if, 它是判断distance(手指的移动距离)是否大于slopSquare(触发移动的最小距离),如果是的话,会回调onScroll方法,并把mAlwaysInTapRegion设为false,这就说明,如果手指按下100秒内开始了拖动的话,那么onSingleTapUp方法也是不会回调的;

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

好,现在onScroll也讲了,轮到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方法就会被回调,当然了,前提是这个任务没有被取消。

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

500ms内有新手指按下;

500ms内触发了onScroll,即手指移动超过指定距离;

500ms内手指抬起;

500ms内收到了ACTION_CANCEL事件(该ACTION一般源自父容器的私自创建);

来看看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分别是怎么处理的:

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是如何支持多指触控的呢?

跟我们平时常用到的RecyclerView、ScrollView、ViewPager之类的容器不同,GestureDetector 并不是在新手指按下后记录它的pointId,然后一直只监听新手指的移动的。

那么,它是怎么做的?

睡觉了,明天再写,嘿嘿。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值