View触发机制API实现GestureDetector OverScroller详解

前言

前一篇文章讲了View的触发反馈机制的原理,对于一个自定义View而言,手势的处理都是重写onTouchEvent函数,或者通过setOnTouchEventListener方法捕捉手势。但是手势的处理,如滑动、触摸、双击等检测对应的检测也并不是那么简单,自己一个个造轮子也过于麻烦,万幸的是google早已经给开发者提供了手势捕捉的类- GestureDetector。通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别。虽然他能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。

GestureDetector

GestureDetector 中一共有三种主要的回调接口 ,OnGestureListenerOnDoubleTapListenerOnContextClickListener

这三个接口的方法如下。

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 负责监听手势,而 OnDoubleTapListenerOnGestureListener 用于开发者自己去处理对应手势的反馈

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
类型触发次数摘要
onSingleTapUp1单击抬起
onSingleTapConfirmed1单击确认
onClick1单击事件

双击事件触发:

onSingleTapUp
onClick
onDoubleTap 
onClick
类型触发次数摘要
onSingleTapUp1在双击的第一次抬起时触发
onSingleTapConfirmed0双击发生时不会触发。
onClick2在双击事件时触发两次。

可以看出来这三个事件还是有所不同的,根据自己实际需要进行使用即可

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 轴上的运动速度(像素/秒)。

利用 velocityXvelocityY 参数可以实现一个具有一定初速度的滑动,之后该速度随着滑动衰减,直到停止。

一般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
作者:陈序猿_Android

%s 个评论

要回复文章请先登录注册