Android手势监听GestureDetector和Scale

作者: zackyG | 来源:发表于2019-01-25 23:26 被阅读42次

    在Android开发中,可能需要实现一些手势监听相关的功能,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。手势监听,还是遵循事件分发和处理的原理。

    GestureDetector

    首先我们来简单了解下,实现双击手势监听需要注意的细节
    1 记录点击事件的时间,双击事件是指快速点击两次触发的事件,记录点击发生的时间,就可以判断两次点击发生的间隔,如果太长肯定不能被视为双击事件。
    2 记录点击事件的次数,双击事件包含两次点击,所以需要判断是否已经有过一次点击。
    3 点击状态重置,在判断双击事件时,不管是否满足触发条件。都要将点击事件的计数器和上一次点击的时间重置。如果触发了双击事件,那计数器次数要归零。如果未触发,计数器应该以本次点击作为双击事件的第一次点击,重新加入后续判断。
    这是我们自定义实现双击事件监听的思路。

    Android系统也封装了GestureDetector,用来实现手势监听。它使用事件分发机制中的MotionEvents来监测各种手势和事件。
    GestureDetector类中包含三个监听器接口,OnGestureListener,OnDoubleTapListener和OnContextClickListener。

    • OnContextClickListener,它是在Android6.0(API 23)才添加的一个选项,是用于检测外部设备上的按钮是否按下的,例如蓝牙触控笔上的按钮,一般情况下,忽略即可。
    • OnDoubleTapListener,用来监听双击事件。有三个回调方法:onDoubleTap,onDoubleTapEvent,onSingleTapConfirm。
    • OnGestureListener,手势检测,主要有以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp)。
    • SimpleOnGestureListener,是包含了以上三个接口的所有监听事件的实现类,它是一个空实现,实际使用中,我们需要继承这个类,并重新需要监听的方法。在创建GestureDetector对象时,可以直接传SimpleOnGestureListener对象,就不用再去单独设置OnDoubleTapListener和OnContextClickListener。
      创建GestureDetector一共有5个构造函数,其中有两个已经废弃,还有一个是重复。主要值得关注的是两个
    GestureDetector(Context context, GestureDetector.OnGestureListener listener)
    GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler)
    

    第二种相比第一种,多了一个Handler参数,这个Handler对象主要是为了给GestureDetector提供一个Looper。
    如果我们是在主线程中创建GestureDetector对象,那么就用第一个构造函数即可,因为此时GestureDetector对象会在内部自动创建一个Handler对对象,这个Handler对象会获取主线程的Looper。然后如果是在一个没有创建Looper的子线程中创建GestureDetector对象,它内部自动创建的Handler,无法获取到当前线程的Looper就会导致创建失败。

    Can't create handler inside thread that has not called Looper.prepare()
    

    如果要在子线程中创建GestureDetector实例,有两种方式
    一种是将主线程中创建的Handler对象作为上面第二个构造方法的参数,创建GestureDetector的实例

    final Handler handler = new Handler();
            Thread thread = new Thread(){
                @Override
                public void run() {
                    super.run();
                    detector = new GestureDetector(AnimationActivity.this,listener,handler);
                }
            };
    

    另一种是在用第一个构造方法创建之前,将线程实例化为Looper。

    Thread thread = new Thread(){
                @Override
                public void run() {
                    Looper.prepare();
                    super.run();
                    detector = new GestureDetector(AnimationActivity.this,listener);
                }
            };
    

    OnDoubleTapListener

    OnDoubleTapListener有三个回调方法,onDoubleTap,onDoubleTapEvent与onSimgleTapConfirmed。
    onDoubleTap和onDOubleTapEvent的区别。如下图所示。onDoubleTap是在双击事件的第二次点击事件序列的ACTION_DOWN事件中触发的。而onDoubleTapEvent事件是在第二次点击事件序列的每一个事件中都会触发。


    image.png

    onSingleTapConfirmed和onClick的区别。这两个回调方法都是监听单击事件。区别在于,onCLick的回调没有延迟,而且是在点击事件序列的ACTION_UP事件触发。onSingleTapConfirmed是由点击事件序列的ACTION_DOWN事件触发,并且有300ms的延迟,主要是为了确认有没有第二次点击,是不是双击事件。


    image.png
    • 需要同时监听单击和双击,则说明单击和双击后响应逻辑不同,然而使用 OnClickListener 会在双击事件发生时触发两次,这显然不是我们想要的结果。而使用 onSingleTapConfirmed 就不用考虑那么多了,你完全可以把它当成单击事件来看待,而且在双击事件发生时,onSingleTapConfirmed 不会被调用,这样就不会引发冲突。
    • 如果是在子线程中创建的GestureDetector对象,而且关联的Looper不是主线程的Looper,将无法触发onSingleTapConfirmed方法。
    • 如果控件设置了onTouchListner,并且onTouch方法返回true,则表示onTouchEvent方法不会触发,自然onCLick方法也不会触发,但是onSIngleTapConfirmed还是可以触发。
    imageview.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    Log.e("onTouch","MotionEvent = "+event.getAction());
                    detector.onTouchEvent(event);
                    scaleDetector.onTouchEvent(event);
                    return true;
                }
            });
    
    image.png

    OnGestureListener

    这个是手势检测中较为核心的一个部分,主要检测以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp)。
    onDown
    监听ACTION_DOWN事件。这个方法的特殊意义在于,在事件分发机制中,通常同一个事件序列中的ACTION_DOWN事件被哪个控件处理(消费)了,那该事件序列的后续事件也由这个控件来处理(消费)。而如果onTown方法返回true,即表示ACTION_DOWN事件被消费掉了。这样的用途是,让一些默认不可点击的控件如ImageView和TextView,具备了消费事件序列的能力。
    onFling
    Fling 中文直接翻译过来就是一扔、抛、甩,最常见的场景就是在 ListView 或者 RecyclerView 上快速滑动时手指抬起后它还会滚动一段时间才会停止。onFling 就是检测这种手势的。

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
            velocityY) {
        return super.onFling(e1, e2, velocityX, velocityY);
    }
    
    • e1,fling手势事件序列开始时的ACTION_DOWN事件
    • e2,fling手势事件序列当前的ACTION_MOVE事件
    • velocityX,fling手势当前在水平方向上的移动速度,单位是每秒多少像素。
    • velocityY,fling手势当前在垂直方向上的移动速度,单位是每秒多少像素。
      onLongPress
      长按事件监听,比较简单。
      onScroll
      监听滚动事件。和onFling比较像。不同的是,onScroll方法的后面两个参数不是速度,而是滚动的距离。
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float 
            distanceY) {
        return super.onScroll(e1, e2, distanceX, distanceY);
    }
    

    onShowPress
    产生ACTION_DOWN但是没有产生ACTION_MOVE和ACTION_UP的情况下触发。用途是在用户按下时,可以产生视觉反馈。如改变控件的背景色或边框颜色。

    @Override 
    public void onShowPress(MotionEvent e) {
    }
    

    不过这个监听和 onSingleTapConfirmed 类似,也是一种延时回调,延迟时间是 180 ms,假如用户手指按下后立即抬起或者事件立即被拦截,时间没有超过 180 ms的话,也就不会触发这个回调。
    onSingleTapUp
    这个也很容易理解,就是用户单击抬起时的回调。当同时监听onSIngleTapUp、onClick、OnSingleTapConfirmed时,他们的触发顺序的:onSIngleTapUp->onClick->onSingleTapConfirmed。值得注意的是,双击事件的第二次点击的ACTION_UP事件不会触发onSingleTapUp。

    ScaleGestureDetector

    缩放手势需要用到的机会比较少,它最常见于以下的一些应用场景中,例如:图片浏览,图片编辑(贴图效果)、网页缩放、地图、文本阅读(通过缩放手势调整文字大小)等。缩放手势相对比较简单,网络上也能查到不少非官方实现的缩放手势计算方案,但部分非官方的方案确实有所局限,例如只支持两个手指的计算,在出现超过两个手指时,只计算了前两个手指的移动,这样显然是不合理的。而ScaleGestureDetector轻松的应对了多个手指的情况。

    ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener)
    ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener, Handler handler)
    

    和GestureDetector一样,他也有两个构造方法,其中上面第二个构造方法的Handler参数的用途也和GestureDetector中的一样。
    ScaleGestureDetector只有一个监听器接口,OnScaleGestureListener。SimpleOnScaleGestureListener是该接口的空实现类。有三个回调方法:

    • onScaleBegin,在缩放手势开始时回调。缩放手势开始,当两个手指放在屏幕上的时候会调用该方法(只调用一次)。如果返回 false 则表示不处理当前这次缩放手势。
    • onScale,缩放手势过程中回调。缩放被触发(会调用0次或者多次),如果返回 true 则表示当前缩放事件已经被处理,检测器会重新积累缩放因子,返回 false 则会继续积累缩放因子。
    • onScaleEnd,在缩放手势结束时回调。
      以下是ScaleGestureDetector的简单用法
    scaleDetector = new ScaleGestureDetector(this,
                new ScaleGestureDetector.SimpleOnScaleGestureListener(){
                    @Override
                    public boolean onScale(ScaleGestureDetector detector) {
                        float xFactor = detector.getCurrentSpanX()/detector.getPreviousSpanX();
                        float yFactor = detector.getCurrentSpanY()/detector.getPreviousSpanY();
                        Log.e("onScale","xFactor = " + xFactor+",yFactor = " + yFactor);
                        Log.e("onScale","scaleFactor = " + detector.getScaleFactor());
                        return super.onScale(detector);
                    }
                    @Override
                    public boolean onScaleBegin(ScaleGestureDetector detector) {
                        return super.onScaleBegin(detector);
                    }
                    @Override
                    public void onScaleEnd(ScaleGestureDetector detector) {
                        super.onScaleEnd(detector);
                    }
                });
    
    基本原理

    上面的代码演示了,ScaleGestureDetector的用法是很简单的,它的实现原理,也并不复杂。对缩放手势的监听,我们需要关心的两个重要因素:一是缩放的中心点,二是缩放比例。

    • 计算缩放手势的中心点。不管是两点触屏还是多点触屏,计算中心点坐标的方式都是将所有点的x坐标和y坐标分别相加,然后取平均值。
    public boolean onTouchEvent(MotionEvent event) {
    ......
    ......
    if (inAnchoredScaleMode()) {
                // In anchored scale mode, the focal pt is always where the double tap
                // or button down gesture started
                focusX = mAnchoredScaleStartX;
                focusY = mAnchoredScaleStartY;
                if (event.getY() < focusY) {
                    mEventBeforeOrAboveStartingGestureEvent = true;
                } else {
                    mEventBeforeOrAboveStartingGestureEvent = false;
                }
            } else {
                for (int i = 0; i < count; i++) {
                    if (skipIndex == i) continue;
                    sumX += event.getX(i);
                    sumY += event.getY(i);
                }
    
                focusX = sumX / div;
                focusY = sumY / div;
            }
    ......
    ......
    }
    
    • 计算缩放比例。就是计算各个点到中心点的平均距离,用当前的平均距离除以此次手势移动前的平均距离。以此计算出缩放比例。
    // 计算到焦点的平均距离
    float devSumX = 0, devSumY = 0;
    for (int i = 0; i < count; i++) {
        if (skipIndex == i) continue;
        devSumX += Math.abs(event.getX(i) - focusX);
        devSumY += Math.abs(event.getY(i) - focusY);
    }
    final float devX = devSumX / div;
    final float devY = devSumY / div;
    
    final float spanX = devX * 2;
    final float spanY = devY * 2;
    final float span;
    if (inAnchoredScaleMode()) {
        span = spanY;
    } else {
        // 相当于 sqrt(x*x + y*y)
        span = (float) Math.hypot(spanX, spanY);
    }
    

    ScaleGestureDetector的onTouchEvent方法会监听,当用户移动的距离超过一定数值(数值大小由系统定义)后,会触发 onScaleBegin 方法,如果用户在 onScaleBegin 方法里面返回了 true,表示接受事件后,就会重置缩放相关数值,并且开始积累缩放比例。

    // mSpanSlop 和 mMinSpan 都是从系统里面取得的预定义数值,该数值实际上影响的是缩放的灵敏度。
    // 不过该参数并没有提供设置的方法,如果对灵敏度不满意的话,则需要自定义一个ScaleGestureDetector的子类, 并且修改其中的数值。
    final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
    if (!mInProgress && span >=  minSpan &&
            (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
        mPrevSpanX = mCurrSpanX = spanX;
        mPrevSpanY = mCurrSpanY = spanY;
        mPrevSpan = mCurrSpan = span;
        mPrevTime = mCurrTime;
        mInProgress = mListener.onScaleBegin(this);
    }
    

    监听缩放手势的移动,回调onScale方法

    if (action == MotionEvent.ACTION_MOVE) {
        mCurrSpanX = spanX;
        mCurrSpanY = spanY;
        mCurrSpan = span;
    
        boolean updatePrev = true;
    
        if (mInProgress) {
            // 注意这里,用户的返回值决定了是否重新计算缩放比例
            updatePrev = mListener.onScale(this);
        }
    
        // 如果用户返回了 true ,就会重新计算缩放比例
        if (updatePrev) {
            mPrevSpanX = mCurrSpanX;
            mPrevSpanY = mCurrSpanY;
            mPrevSpan = mCurrSpan;
            mPrevTime = mCurrTime;
        }
    }
    

    以上就是ScaleGestureDetector实现缩放手势监听的原理介绍,推荐去看一下源码,源码的逻辑也非常简洁明了,相信看完之后也就能够彻底理解它的原理了。

    本文参考:
    http://www.gcssloop.com/customview/gestruedector
    http://www.gcssloop.com/customview/scalegesturedetector

    相关文章

      网友评论

        本文标题:Android手势监听GestureDetector和Scale

        本文链接:https://www.haomeiwen.com/subject/iyzrjqtx.html