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


猜你喜欢
- 前言研究表明,Java堆中对象占据最大比重的就是字符串对象,所以弄清楚字符串知识很重要,本文主要重点聊聊字符串常量池。Java中的字符串常量
- 本文通过一个简单的小例子简述SharpZipLib压缩文件的常规用法,仅供学习分享使用,如有不足之处,还请指正。什么是SharpZipLib
- 目录一、概述二、环境配置及代码步骤1. 环境配置2. 代码步骤一、概述PDF打印小册子是指将PDF格式文档在打印成刊物前需要提前进行的页面排
- 一、需求触发场景:项目中需要开发带有EditText的Dialog显示,要求在编辑完EditText时,点击Dilog的空白处隐藏软键盘。但
- 流程控制语句是C语言中最基本的判断语句,通常我们可以使用IF来构建多分支结构,但同样可以使用Switch语句构建,Switch语句针对多分支
- JavaWeb项目部署到服务器详细步骤本地准备在eclipse中将项目打成war文件:鼠标右键要部署到服务器上的项目导出项目数据库文件MyS
- 一. 析构方法1. 概念我们现在已经知道,构造方法负责创建一个Java的类对象,并可以对该对象进行初始化。与此相对应的,其实还有一个方法,可
- Android利用爬虫实现模拟登录的实现实例为了用手机登录校网时不用一遍一遍的输入账号密码,于是决定用爬虫抓取学校登录界面,然后模拟填写本次
- 以下内容给大家介绍Android数据存储提供了五种方式:1、SharedPreferences2、文件存储3、SQLite数据库4、Cont
- 本文实例为大家分享了android选择视频文件上传到后台服务器的具体代码,供大家参考,具体内容如下选择本地视频文件附上Demo首先第一步打开
- 本文实例为大家分享了Android实战闹钟项目的具体代码,供大家参考,具体内容如下一、闹钟功能的介绍以及界面的展示该闹钟是根据我们手机闹钟设
- 目录一、定义联合(union)二、初始化联合(union)三、联合体变量的声明四、联合体变量的赋值和使用五、struct和union和区别u
- Controller @RequestMapping作用@RequestMapping是一个用来处理请求地址映射的注解,可用于类或者方法上。
- 本文以C#及VB.NET后端程序代码示例展示如何将HTML转为XML文件。转换时,调用Word API -Free Spire.Doc fo
- 目录引言编译环境及说明图片素材分割事件处理OnPaint事件鼠标交互事件代码汇总引言我们有时候会在程序的文件夹里看见一些图标,而这些图标恰好
- 一、图片预览:一、实现功能:需求要实现布局中为圆形图片,图片背景与图标分开且合并到一个ImageView。二、具体实现:XML中布局中定义I
- 在我们工作中涉及到一些场景需要我们配置多数据源的操作,之前来说我们配置数据源需要写繁琐的配置类来配置我们的数据源,哪个是默认数据源等等,而现
- 一、strcmp函数适用对象char*类型字符串函数介绍strcmp函数是cstring库中的函数,包含在string.h头文件中用法str
- spring boot security设置忽略地址不生效最近在试下微服务改造,出现这样一个问题所有请求都经过spring cloud ga
- 一、自定义Dialog在沉浸式效果下,当界面弹出对话框时,对话框将获取到焦点,这将导致界面退出沉浸式效果,那么是不是能通过屏蔽对话框获取焦点