GestureDetector与ScaleGestureDetector入门

2,552 阅读10分钟
原文链接: blog.csdn.net

GestureDetector

概述

          该类主要是用于识别一些特定的手势,我们只需要调用GestureDetector.onTouchEvent(),并把MotionEvent传递进去即可。对于各种手势的回调,可以通过GestureDetector中的接口OnGestureListener来完成。

        只需要在View#onTouchEvent()中调用GestureDetector#onTouchEvent()即可。

OnGestureListener

onDown

/**
         * Notified when a tap occurs with the down {@link MotionEvent}
         * that triggered it. This will be triggered immediately for
         * every down event. All other events should be preceded by this.
         *
         * @param e The down motion event.
         */
        boolean onDown(MotionEvent e);

        在GestureDetector.onTouchEvent()中,当action_down发生时,该方法肯定会立即被调用。源码为:

    case MotionEvent.ACTION_DOWN:
        ……
        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;
        从中可以看出action_down事件发生时,一定会触发onDown()事件。而隔了TAG_TIMEOUT后才会触发onShowPress();隔TAP_TIMEOUT+LONGPRESS_TIMEOUT后才会触发onLongPress()。

        GestureDetector.onTouchEvent()的返回值就是最后一句中的handled,而在每一次调用该方法时handled都会被初始化成false。因此,该方法最终会影响onTouchEvent()的返回值,如果onDown()返回true,那么onTouchEvent()一定会返回true;反之不一定返回false(因为省略的部分对handled进行了修改)。

onShowPress 

/**
         * The user has performed a down {@link MotionEvent} and not performed
         * a move or up yet. This event is commonly used to provide visual
         * feedback to the user to let them know that their action has been
         * recognized i.e. highlight an element.
         *
         * @param e The down motion event
         */
        void onShowPress(MotionEvent e);
        在action_down时,会通过handler发送一个消息(SHOW_PRESS),代码为:
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
        从这行代码可以看出,在down事件发生时,并不会立即执行onShowPress(),会等待一段时间后才执行。并且如果在该时间段内,发生了action_move,action_up或者ACTION_CANCEL,还会取消执行该方法。因此,该方法就是在用户真正进行按下事件时才执行,并不是ACTION_DOWN时。因为ACTION_DOWN后,用户可能进行move也可能进行press。

        onDown()会在action_down事件时就执行,无论该次触摸会发展成press还是scroll。而onShowPress只有在事件是press(按下足够长的时间,并且没有大范围移动)时才会执行,在该方法中一般用来提示用户你已按下,而不能用来单击事件的操作,因为有可能会发现成onLongPress。

onLongPress

        与onShowPress()类似,只不过按的时候长时执行onLongPress,时间短时执行onShowPress()。从onDown中的源码可以看出,当mIsLongpressEnabled=true时,才有可能执行onLongPress()。

        在new GestureDetector时,该值默认设置为true。可以通过setIsLongpressEnabled()进行设置。

onSingleTapUp

/**
         * Notified when a tap occurs with the up {@link MotionEvent}
         * that triggered it.
         *
         * @param e The up motion event that completed the first tap
         * @return true if the event is consumed, else false
         */
        boolean onSingleTapUp(MotionEvent e);
      在GestureDetector.onTouchEvent()中,只有在action_up时,才会有可能执行该方法。但是它并不等价于action_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) {
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion) {
                handled = mListener.onSingleTapUp(ev);

        从中可以看出,只有当mIsDoubleTapping=false,mInLongPress=false且mAlwaysInTapRegion=true时才会执行。

        在源码中可以看到,当移动的距离过大(移动距离超出以ACTION-DOWN时为圆心,以ViewConfiguration.get(context).getScaledTouchSlop();为半径的圆时,移动距离就过大)时mAlwaysInTapRegion = false,或者当点击的时间太长时mInLongPress = true,当为双击事件时,mIsDoubleTapping为true,此时都不会执行onSingleTapUp()。因此,该方法在短单击事件抬起时执行,而且它的MotionEvent中只有Action_UP事件。

        注:ViewConfiguration.get(context).getScaledTouchSlop()的返回值经常用来判断是移动还是点击。用户按下后,肯定会不停的抖动,抖动的过程中会不断地触发ACTION_MOVE。如果直接将两次ev.getX(),ev.getY()不一样就认为是滑动的话,显然是不合理的。合理的做法是:当两次ev.getX(),ev.getY()的距离够大才认为是滑动,而ViewConfiguration.get(context).getScaledTouchSlop()就是对这个距离的判断标准。

onScroll

 /**
         * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
         * current move {@link MotionEvent}. The distance in x and y is also supplied for
         * convenience.
         *
         * @param e1 The first down motion event that started the scrolling.
         * @param e2 The move motion event that triggered the current onScroll.
         * @param distanceX The distance along the X axis that has been scrolled since the last
         *              call to onScroll. This is NOT the distance between {@code e1}
         *              and {@code e2}.
         * @param distanceY The distance along the Y axis that has been scrolled since the last
         *              call to onScroll. This is NOT the distance between {@code e1}
         *              and {@code e2}.
         * @return true if the event is consumed, else false
         */
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
      当有滚动时,执行该方法。对该方法的调用都出现在action_move中,代码为:
else if (mAlwaysInTapRegion) {
                final int deltaX = (int) (x - mCurrentDownEvent.getX());
                final int deltaY = (int) (y - mCurrentDownEvent.getY());
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                if (distance > mTouchSlopSquare) {//移动距离过大
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mAlwaysInTapRegion = false;
                    mHandler.removeMessages(TAP);
                    mHandler.removeMessages(SHOW_PRESS);//移除对onShowPress的回调
                    mHandler.removeMessages(LONG_PRESS);//移除longpress
                }
                if (distance > mBiggerTouchSlopSquare) {
                    mAlwaysInBiggerTapRegion = false;
                }
            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastMotionX = x;
                mLastMotionY = y;
            }
       对于mAlwaysInTapRegion,当action_down时,它会被设置为true。以后只要移动距离不过大,mAlwaysInTapRegion就会一直为true。这也是一般处理点击和滑动的方式:只要移动范围过大了,就不会再认为是点击 。只要移动范围不过大,都认为是点击中,不论调用几次action_move。

onFling

        手指离开屏幕,且滑动速度足够快时调用且仅调用一次该方法。注意:这个方法并不是当屏幕滚动时调用的。

final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                final float velocityY = velocityTracker.getYVelocity();
                final float velocityX = velocityTracker.getXVelocity();

                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
                }
        从中可以看到只有当x轴速度或y轴速度大于相应的值时,才会调用onFling()。

OnDoubleTapListener

          它是GestureDetector中的另一个接口,主要用来处理双击事件。可以通过调用setOnDoubleTapListener()进行设置,也可以在new GestureDetector()时传入SimpleOnGestureListener。

onSingleTapConfirmed

/**
         * Notified when a single-tap occurs.
         * <p>
         * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
         * will only be called after the detector is confident that the user's
         * first tap is not followed by a second tap leading to a double-tap
         * gesture.
         *
         * @param e The down motion event of the single-tap.
         * @return true if the event is consumed, else false
         */
        boolean onSingleTapConfirmed(MotionEvent e);
          当确定该事件为短单击事件时执行该方法。它与OnGestureListener.onSingleTapUp类似,但是又有不同。onSingleTapUp当一次短点击事件执行到action_up时就会执行;而onSingleTapConfirmed却是在确定该次短点击事件是一次单击事件时才会执行,如果是双击事件就不会执行。

        当我们双击时,只会执行onSingleTapUp,不会执行onSingleTapConfirmed;当我们单击时,两者都会执行。

onDoubleTap

        /**
         * Notified when a double-tap occurs.
         *
         * @param e The down motion event of the first tap of the double-tap.
         * @return true if the event is consumed, else false
         */
        boolean onDoubleTap(MotionEvent e);
       双击时,第二次touch事件的down发生时就会执行。因此该MotionEvent中只有action_down。因为该方法只有在action_down中被调用。

onDoubleTapEvent   

/**
         * Notified when an event within a double-tap gesture occurs, including
         * the down, move, and up events.
         *
         * @param e The motion event that occurred during the double-tap gesture.
         * @return true if the event is consumed, else false
         */
        boolean onDoubleTapEvent(MotionEvent e);
        该方法与onDoubleTap类似,只不过它的参数中含有action_up,action_down,action_move事件。它的源码是:

action_down时:

// 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);
        从这里可以看出:先调用onDoubleTap再调用onDoubleTapEvent。

action_move时:

 if (mIsDoubleTapping) {
                // Give the move events of the double-tap
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mAlwaysInTapRegion) {
       从中可以看出:只有当mIsDoubleTapping = true时,才会执行该方法。

       而mIsDoubleTapping = true的唯一一个地方就是上面action_down中的代码。而action_up也类似于action_move。

           因此,可以看出:onDoubleTapEvent的执行后于onDoubleTap,并且它的MotionEvent中的down,move与up都是在双击事件中的第二击中的。

总结

        单击:选择onSingleTapUp。

        长按:选择onLongPress。

        展示按下效果:选择onShowPress()——该方法中有show,主要就是用来展示效果的。不能在该方法中处理单击事件,因为有可能会发展成LongPress。

        拖动:选择onScroll。

        快速滑动:选择onFling。

        双击:选择onDoubleTap。这种选择也不好,但几个方法中最好的。

ScaleGestureDetector

概述

        用于处理缩放的工具类,用法与GestureDetector类似,都是通过onTouchEvent()关联相应的MotionEvent的。使用该类时,用户需要传入一个完整的连续不断地motion事件(包含ACTION_DOWN,ACTION_MOVE和ACTION_UP事件)。

OnScaleGestureListener

        ScaleGestureDetector中的回调接口。主要有三个方法。

onScale

        缩放时。返回值代表本次缩放事件是否已被处理。如果已被处理,那么detector就会重置缩放事件;如果未被处理,detector会继续进行计算,修改getScaleFactor()的返回值,直到被处理为止。因此,它常用在判断只有缩放值达到一定数值时才进行缩放。例如:

public boolean onScale(ScaleGestureDetector detector) {
		System.out.println(detector.getScaleFactor());
		if(detector.getScaleFactor()< 2){
			return false;
		}
		mMatrix.preScale(detector.getScaleFactor(), detector.getScaleFactor());
		setImageMatrix(mMatrix);
		return true;
	}
        当进行放大时它的输出结果为:

        从中可以看出getScaleFactor()返回的值由小变大,直到大于2后又从1开始变大。这是因为,当缩放因子(getScaleFactor()的返回值)小于2时,onScale()返回的是false,此时detector认为本次缩放尚未结束,所以再次计算缩放因子时仍旧以上次结束时为基准,这样就导致了手指外移时缩放参数越来越大;当达到2后,本次缩放结束,再次获得缩放因子时的基准就变成刚刚结束的位置了,因此,获取的缩放因子会猛然变小(因为手指外移,执行的是放大操作,所以缩放因子会大于1)。

onScaleBegin

        缩放开始。该detector是否处理后继的缩放事件。返回false时,不会执行onScale()。

onScaleEnd

        缩放结束时。

常用方法

        参考:www.haodaima.net/art/1742556

        onTouchEvent():关联MotionEvent。返回true代表该detector想继续接收后继的motion事件;否则反之。默认时该方法返回true。

        getScaleFactor():获取本次缩放事件的缩放因子(缩放事件以onScale()返回值为基准,一旦该方法返回true,代表本次事件结束,要开启下次事件)。它的返回值是指本次事件中的缩放值,并不是相对于最开始的值。如一张图片开始放大2倍,后来又放大1.1倍。那么第二次放大时,该方法返回的就是1.1,而不是总放大倍数:2*1.1。

        getCurrentSpan(): 返回手势过程中,组成该手势的两个触点的当前距离。 返回值以像素为单位的触点距离。

        getCurrentSpanX(),getCurrentSpanY():跟getCurrentSpan()类似,只不过一个是返回的是x轴上的距离,一个是y轴上的距离。注意:返回值有可能为负数。这两个方法的返回值和getCurrentSpan()的返回值满足勾股定理。

        getFocusY(),getFocusX():返回组成该手势的两个触点的中点在组件上的y,x轴坐标,单位为像素。

        getPreviousSpan():返回缩放过程中,组成当前缩放手势的两个触点的前一次距离。假设有a,b,c三个手指,某一次a,b组成缩放手势,两者的距离是300;随后一直是b,c组成缩放手势,当c抬起时,b,c的距离时100。此时,ab会组成缩放手势,该值返回的就是300,而不是b,c的100。

        getPreviousSpanX(),getPreviousSpanY():同getPreviousSpan()类似。

        getEventTime():获取当前motion事件的时间。源码如下:

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

        mCurrTime = event.getEventTime();//该方法返回的就是mCurrTime的值
        getTimeDelta():返回上次缩放事件结束时到当前的时间间隔。见下图:


        开始是1586,下一次立刻变成了10。这是因为在1586时,onScale返回了true,detector认为本次缩放事件已结束,再获取deltatime时就重新开始计算,所以值陡然变减少变成10。

        isInProgress():如果缩放手势正处在进行中,返回true;否则返回false。