View触发机制API实现GestureDetector OverScroller详解
作者:陈序猿_Android 时间:2023-01-24 00:57:08
前言
前一篇文章讲了View的触发反馈机制的原理,对于一个自定义View而言,手势的处理都是重写onTouchEvent函数,或者通过setOnTouchEventListener方法捕捉手势。但是手势的处理,如滑动、触摸、双击等检测对应的检测也并不是那么简单,自己一个个造轮子也过于麻烦,万幸的是google早已经给开发者提供了手势捕捉的类- GestureDetector
。通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别。虽然他能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。
GestureDetector
在GestureDetector
中一共有三种主要的回调接口 ,OnGestureListener
、OnDoubleTapListener
、OnContextClickListener
这三个接口的方法如下。
public interface OnGestureListener {
boolean onDown(MotionEvent e);
void onShowPress(MotionEvent e);
boolean onSingleTapUp(MotionEvent e);
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
void onLongPress(MotionEvent e);
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
boolean onSingleTapConfirmed(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onDoubleTapEvent(MotionEvent e);
}
public interface OnContextClickListener {
boolean onContextClick(MotionEvent e);
}
GestureDetector 使用
GestureDector
负责监听手势,而 OnDoubleTapListener
、OnGestureListener
用于开发者自己去处理对应手势的反馈
package com.example.androidtemp.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.OverScroller;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
public class TouchView extends View implements GestureDetector.OnGestureListener,GestureDetector.OnDoubleTapListener{
private static final String TAG = "TouchView";
GestureDetector gestureDetector = null;
public TouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
gestureDetector = new GestureDetector(context,this);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i(TAG, "onSingleTapConfirmed: ");
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i(TAG, "onDoubleTap: ");
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.i(TAG, "onDoubleTapEvent: ");
return false;
}
@Override
public boolean onDown(MotionEvent e) {
Log.d(TAG, "onDown: ");
return true;
}
@Override
public void onShowPress(MotionEvent e) {
Log.i(TAG, "onShowPress: ");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.i(TAG, "onSingleTapUp: ");
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.i(TAG, "onScroll: ");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i(TAG, "onLongPress: ");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i(TAG, "onFling: ");
return false;
}
}
onDown方法
onDown
方法是在ACTION_DOWN
事件时被调用的,其的返回值决定了View
是否消费该事件,一般我们肯定是需要消费该事件的,因此其值为true.
public boolean onDown() {
return true;
}
onShowPress方法
@Override
public void onShowPress(MotionEvent e) {
//进行控件颜色的改变或其他一些动作
}
onShowPress
是用户按下时的一种回调,主要作用是用于给用户一种按压下的状态,可以在该回调中让控件颜色改变或进行一些动作。需要注意的是,onShowPress 方法不是立即回调的,在手指触碰后,在100ms左右后才会回调。在这100ms内如果手指抬起或滚动,该回调方法不会被触发。在前一篇文章View事件分发机制
中提到过自定义View
默认的super.onTouchEvent
实现中,按压状态也是有一个预按压状态的检测,此处的onShowPress
的回调机制也是同理。
onLongPress 方法
用于检测长按事件的,即手指按下后不抬起,在一段时间后会触发该事件。
@Override
public void onLongPress(MotionEvent e) {
}
onLongPress
回调被触发前 onShowPress
一定会被触发。
需要注意的是 onLongPress
一旦被触发,其他事件都不会被触发了。
不过,onLongPress
事件可以被禁止使用,通过如下代码设置,即不会触发长按事件
gestureDetector.setIsLongpressEnabled(false);
onSingleTapUp 方法
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
onSingleTapUP
的返回值不是太重要,不过一般消费了就还是返回ture吧。
onSingleTapUp
的意思顾名思义,即在 手指抬起时触发,不过他跟一般的onClick
、以及onSingleTapConfirmed
有一定区别
单击事件触发:
GCS: onSingleTapUp
GCS: onClick
GCS: onSingleTapConfirmed
类型 | 触发次数 | 摘要 |
---|---|---|
onSingleTapUp | 1 | 单击抬起 |
onSingleTapConfirmed | 1 | 单击确认 |
onClick | 1 | 单击事件 |
双击事件触发:
onSingleTapUp
onClick
onDoubleTap
onClick
类型 | 触发次数 | 摘要 |
---|---|---|
onSingleTapUp | 1 | 在双击的第一次抬起时触发 |
onSingleTapConfirmed | 0 | 双击发生时不会触发。 |
onClick | 2 | 在双击事件时触发两次。 |
可以看出来这三个事件还是有所不同的,根据自己实际需要进行使用即可
onScroll
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float
distanceY) {
return true;
}
onScroll
方法是用于监听手指的滑动的,e1是第一次ACTION_DOWN
的事件,e2是当前滚动事件。distanceX、distanceY记录了手指在x、y轴滑动的距离。
需要注意的时,该滑动距离记录的是上次滑动回调与这次回调之间的距离差值。且还有一个有意思的注意事项,该差值是 lastEvent-curEvent 得到的,这与正常的逻辑行为不太一致,不过google就这样干了,所以当我们在计算滑动偏移量时需要对 distanceX、distancesY进行一个 相减的操作而不是相加。
onFling
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return true;
}
用户手指在屏幕快速滑动后,在抬起时(ACTION_UP
)触发该事件。
Fling 中文直接翻译过来就是一扔、抛、甩,最常见的场景就是在 ListView 或者 RecyclerView 上快速滑动时手指抬起后它还会滚动一段时间才会停止。onFling 就是检测这种手势的。
四个参数的介绍如下
参数 | 简介 |
---|---|
e1 | 手指按下时的 Event。 |
e2 | 手指抬起时的 Event。 |
velocityX | 在 X 轴上的运动速度(像素/秒)。 |
velocityY | 在 Y 轴上的运动速度(像素/秒)。 |
利用 velocityX
、velocityY
参数可以实现一个具有一定初速度的滑动,之后该速度随着滑动衰减,直到停止。
一般onFling
可以结合 OverScroller
实现一个均匀减速的滑动效果。
overScroller
的用法在后方介绍。
onSingleTapConfirmed 和onDoubleTap
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
public boolean onDoubleTap(MotionEvent e) {
return false;
}
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
onSingleTapConfirmed
用于监听单击事件,而onDoubleTap
用于监听双击事件。这两个回调函数是互斥的。
onSingleTapConfigrmed
的调用是延迟的,其在 手指按下300ms后触发。
onSingleTapConfigrmed
适合于在 既检测单击事件也检测双击时间时使用。
但是如果只是检测单击事件,onSingleTapUp
更合适,onSingleTapConfigrmed
会让用户明显感觉到延迟。
需要注意的是 onDoubleTap
事件并不是第二次抬起时触发的,而是第二次手触摸到屏幕时即(第二次ACTION_DOWN)事件时就会触发该事件,如果要保证在第二次抬起时才触发该事件,就需要使用onDoubleTapEvent
方法了
onDoubleTapEvent
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.i(TAG, "onDoubleTapEvent: event:" + e.getActionMasked());
switch (e.getActionMasked()) {
case MotionEvent.ACTION_UP:
Log.i(TAG, "onDoubleTapEvent: ACTION_UP");
break;
}
return true;
}
双击时,onDoubleTapEvent
将会在onDoubleTap
后触发.
双击触发日志:
TouchView: onDown:
TouchView: onSingleTapUp:
TouchView: onDoubleTap:
TouchView: onDoubleTapEvent: event:0(ACTION_DOWN)
TouchView: onDown:
TouchView: onDoubleTapEvent: event:2(ACTION_MOVE)
TouchView: onDoubleTapEvent: event:2(ACTION_MOVE)
TouchView: onDoubleTapEvent: event:1(ACTION_UP)
TouchView: onDoubleTapEvent: ACTION_UP
需要注意的是不论是双击还是单击,只要按下长时间未动且未抬起,都会触发onLongPress
。
第二次按下后常按再抬起日志
TouchView: onDown:
TouchView: onSingleTapUp:
TouchView: onDoubleTap:
TouchView: onDoubleTapEvent: event:0
TouchView: onDown:
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onShowPress:
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onLongPress:
ouchView: onDoubleTapEvent: event:1
TouchView: onDoubleTapEvent: ACTION_UP
OverScroller
在 onFling
方法中,曾说过 使用velocityX
,velocityY
两个参数可以实现 View
的滑动效果.
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return true;
}
示例
此处用一个可拖拉滑动的小圆球作为示例.
scroll效果图
Fling效果图
代码如下
package com.example.androidtemp.view
import android.view.View
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.OverScroller
import kotlin.math.max
import kotlin.math.min
private const val TAG = "SmallBallView"
class SmallBallView(context: Context?, attrs:AttributeSet?) :View(context,attrs) ,GestureDetector.OnGestureListener{
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val BALL_DIAMETER_SIZE = 100 //球直径长度
private var originOffsetX = 0f
private var originOffsetY = 0f
private var offsetX = 0f
private var offsetY = 0f
private val gestureDetector = GestureDetector(this.context,this)
private val scroller = OverScroller(this.context)
override fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event);
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
originOffsetX = (w - BALL_DIAMETER_SIZE)/2f
originOffsetY = (h - BALL_DIAMETER_SIZE)/2f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 偏移
canvas.translate(offsetX,offsetY)
//中间位置画个圆
canvas.drawArc(originOffsetX,originOffsetY,originOffsetX + BALL_DIAMETER_SIZE.toFloat(),originOffsetY + BALL_DIAMETER_SIZE.toFloat(),0f,360f,false,paint)
}
override fun onDown(e: MotionEvent?): Boolean = true
override fun onShowPress(e: MotionEvent?) {}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}
override fun onLongPress(e: MotionEvent?) {}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
Log.i(TAG, "onScroll: ")
offsetX -= distanceX
offsetY -= distanceY
//移动不能超过圆的一半
offsetX = min(offsetX,width.toFloat()/2)
offsetX = max(offsetX,-width.toFloat()/2)
//移动不能超过圆的一半
offsetY = min(offsetY,height.toFloat()/2)
offsetY = max(offsetY,-height.toFloat()/2)
invalidate()
return true;
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
//限制滑动不能超过一小圆的一半
scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),-width/2,width/2,-height/2,height/2)
postOnAnimation(scrollerRunnable)
return true;
}
private val scrollerRunnable = object :Runnable {
override fun run() {
if (scroller.computeScrollOffset()) {
offsetX = scroller.currX.toFloat()
offsetY = scroller.currY.toFloat()
invalidate()
postOnAnimation(this)
}
}
}
}
OverScroller方法介绍
fling
方法
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
}
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
//实现逻辑省略,有兴趣的可以自己去看代码
}
参数 | 简介 |
---|---|
startX、startY | 开始滑动的X(Y)轴位置 |
velocityX、velocityY | 在 X(Y) 轴上的运动速度(像素/秒)。 |
minX、maxX | 滑动时X轴的两个边界值,滑动时一旦到达边界值,则立刻停止 |
minY、maxY | 滑动时Y轴的两个边界值,滑动时一旦到达边界值,则立刻停止 |
overX、overY | 在滑动时,可超出的滑动值,可超过边界值,不过超过边界值后,又会重新滑动回来 |
startScroll
方法
startScroll
的滚动默认以一种粘性液体的效果进行滚动。
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);//DEFAULT_DURATION 250 ms
}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mScrollerX.startScroll(startX, dx, duration);
mScrollerY.startScroll(startY, dy, duration);
}
参数 | 简介 |
---|---|
startX、startY | 开始滑动的X(Y)轴位置 |
dx、dy | 滚动到达的目标位置 |
duration | 滚动花费时间(单位ms),如果不指定默认时250ms |
来源:https://juejin.cn/post/7167720906540711966