Android手势监听GestureDetector和ScaleGestureDetector

在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

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

推荐阅读更多精彩内容