在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事件是在第二次点击事件序列的每一个事件中都会触发。
onSingleTapConfirmed和onClick的区别。这两个回调方法都是监听单击事件。区别在于,onCLick的回调没有延迟,而且是在点击事件序列的ACTION_UP事件触发。onSingleTapConfirmed是由点击事件序列的ACTION_DOWN事件触发,并且有300ms的延迟,主要是为了确认有没有第二次点击,是不是双击事件。
- 需要同时监听单击和双击,则说明单击和双击后响应逻辑不同,然而使用 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;
}
});
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