敞开生长之旅!这是我参与「日新计划 12 月更文挑战」的第1天,点击查看活动概况

前语

前一篇文章讲了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
类型 触发次数 摘要
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 轴上的运动速度(像素/秒)。

运用 velocityXvelocityY 参数能够完成一个具有必定初速度的滑动,之后该速度随着滑动衰减,直到中止。

一般onFling 能够结合 OverScroller 完成一个均匀减速的滑动作用。

overScroller的用法在后方介绍。

onSingleTapConfirmedonDoubleTap

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 事情并不是第2次抬起时触发的,而是第2次手接触到屏幕时即(第2次ACTION_DOWN)事情时就会触发该事情,假如要确保在第2次抬起时才触发该事情,就需求运用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

第2次按下后常按再抬起日志

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作用图

View 触发机制实现相关API(GestureDetector、OverScroller))

Fling作用图

View 触发机制实现相关API(GestureDetector、OverScroller))

代码如下

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办法介绍

  1. 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 在滑动时,可超出的滑动值,可超过鸿沟值,不过超过鸿沟值后,又会重新滑动回来
  1. 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