在 Android 开发当中,View 的事件分发机制是一块很重要的知识。不仅在开发当中经常需要用到,面试的时候也经常被问到。
如果你在面试的时候,能把这块讲清楚,对于校招生或者实习生来说,算是一块不错的加分项。对于工作几年的我们来说,这是必须掌握的,讲不明白,那你回去等通知吧,哈哈。
![](https://img-blog.csdnimg.cn/img_convert/e4d40b52cbeff75b3cc67462335e63eb.png)
目录大概如下:
View 事件分发机制简介
View 常见滑动冲突解决
View 双击,多击事件是怎么实现的
手势识别
小结
View 事件分发机制简介
View 触摸事件
对于屏幕的点击,滑动,抬起等一系的动作,其实都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:
1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件
4.ACTION_CANCEL 当前 View 的手势被打断,后续不会再收到任何事件
从 ACTION_DOWN 开始到 ACTION_UP/ACTION_CANCEL 结束我们称为一个事件序列
正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面 3 种 case。
点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP
点击后再风骚的滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
某些情况下,我们可能会没有收到 ACTION_UP 事件,是收到 ACTION_CANCEL 事件。
ACTION_CANCEL 一般是指 ChildView 原先拥有事件处理权,后面由于某些原因,该处理权需要交回给上层去处理,ChildView便会收到ACTION_CANCEL事件。对于一些复位或者重置操作,我们应该在 ACTION_UP 和 ACTION_CANCEL 里面同时进行处理。
代码逻辑上是:上层判断之前交给ChildView的事件处理权需要收回来了,便会做事件的拦截处理,拦截时给ChildView发一个ACTION_CANCEL事件
几个主要方法
我们知道,View 的事件分发机制主要涉及到以下几个方法
dispatchTouchEvent ,这个方法主要是用来分发事件的
onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是 ViewGroup 才有这个方法,View 没有 onInterceptTouchEvent 这个方法)
onTouchEvent 这个方法主要是用来处理事件的
requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true 表示父 View 不拦截事件,false 表示父 View 拦截事件
我们先来看一张图。
以下内容参考图解 Android 事件分发机制这一篇博客
![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/2d54e2b3cd3c537b0b45352f38d91b55.png)
在这里插入图片描述
仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 进行分发
箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。)
dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
目前所有的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP我们最后做分析。
当触摸事件发生时,首先 Activity 将 TouchEvent 传递给最顶层的 View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发,
如果dispatchTouchEvent返回true 消费事件,事件终结。
如果dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;
如果dispatchTouchEvent返回super的话,默认会调用自己的onInterceptTouchEvent方法。
默认的情况下onInterceptTouchEvent回调用super方法,super方法默认返回false,所以会交给子View的onDispatchTouchEvent方法处理
如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,
如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。
关于更多详细分析,请查看原博客图解 Android 事件分发机制,真心推荐,写得很好。
View 滑动事件冲突
在开发当中,View 的滑动冲突时经常遇到的,比如 ViewPager 嵌套 ViewPager,ScrollView 嵌套 ViewPager。下面让我们一起来看看怎么解决。
常见的三种情况
第一种情况,滑动方向不同
![](https://img-blog.csdnimg.cn/img_convert/88af585fcadc8985c29be90dd5f698e2.png)
第二种情况,滑动方向相同
![](https://img-blog.csdnimg.cn/img_convert/dbab434ccda12d5fbadc1b19d88bcca8.png)
第三种情况,上述两种情况的嵌套
![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/24f5568f56ca713b6b6c282743b5aed8.png)
在这里插入图片描述
解决思路
看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突。
那既然同一时刻只能由某一个 View 或者 ViewGroup 消费拦截,那我们就只需要 决定在某个时刻由这个 View 或者 ViewGroup 拦截事件,另外的 某个时刻由 另外一个 View 或者 ViewGroup 拦截事件,不就 OK了吗?
综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种解决方案
以下解决思路来自于 《Android开发艺术》 书籍
下面的两种方法针对第一种情况(滑动方向不同),父View是上下滑动,子View是左右滑动的情况。
外部解决法
从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不要的时候返回false,为代码大概 如下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final float deltaY = Math.abs(y - mDownPosY);
// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
if (deltaX > deltaY) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
内部解决法
从子View着手,父View先不要拦截任何事件,所有的事件传递给 子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View处理。
实现思路 如下,重写子 View的dispatchTouchEvent方法,在Action_down 动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父 View不要拦截事件,这样保证子 View 能够接受到 Action_move 事件,再在 Action_move 动作中根据自己的逻辑是否要拦截事件,不需要拦截事件的话再交给 父 View 处理。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// 保证子View能够接收到Action_move事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
更多细节,可以查看我的这篇文章,里面有详细介绍哦
ViewPager,ScrollView 嵌套ViewPager滑动冲突解决
View 双击,多击事件是怎么实现的
实现之前,我们首先来阐述一下思路,怎样实现双击事件,正所谓,授人以鱼不如授人以渔。
单击:用户点击一次之后,一段时间之内不再点击
双击;用户点击一次之后,一段时间之内再次点击
实现思路
我们监听 onTouch 事件,在 ACTION_DOWN 的时候,点击次数 clickCount +1;
同时,在 ACTION_DOWN 的时候,延时一段时间,执行相应的 Runnable 任务,这里我们用 handler 的 postDelayed 实现
在延时任务执行的时候,我们根据点击的次数,进行单击或者多级的回调,最后,记得重置点击次数,以及移除延时任务
open class MyDoubleTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {
private var clickCount = 0 //记录连续点击次数
private val handler: Handler = Handler()
interface MyClickCallBack {
fun oneClick() //点击一次的回调
fun doubleClick() //连续点击两次的回调
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
clickCount++
handler.postDelayed({
if (clickCount == 1) {
myClickCallBack.oneClick()
} else if (clickCount == 2) {
myClickCallBack.doubleClick()
}
handler.removeCallbacksAndMessages(null)
//清空handler延时,并防内存泄漏
clickCount = 0 //计数清零
}, timeout.toLong()) //延时timeout后执行run方法中的代码
}
return false //让点击事件继续传播,方便再给View添加其他事件监听
}
companion object {
private const val TAG = "MyClickListener"
private val timeout = ViewConfiguration.getDoubleTapTimeout() //双击间四百毫秒延时
init {
Log.i(TAG, "timeout is $timeout ")
}
}
}
三击事件
三级事件呢,其实也很简单,我们直接判断在指定时间间隔内点击的次数即可
open class MyMultiTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {
private var clickCount = 0 //记录连续点击次数
private val handler: Handler = Handler()
interface MyClickCallBack {
fun oneClick() //点击一次的回调
fun doubleClick() //连续点击两次的回调
fun threeClick() // 连续点击三次的回调
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
clickCount++
handler.postDelayed({
if (clickCount == 1) {
myClickCallBack.oneClick()
} else if (clickCount == 2) {
myClickCallBack.doubleClick()
} else if (clickCount == 3) {
myClickCallBack.threeClick()
}
handler.removeCallbacksAndMessages(null)
//清空handler延时,并防内存泄漏
clickCount = 0 //计数清零
}, timeout.toLong()) //延时timeout后执行run方法中的代码
}
return false //让点击事件继续传播,方便再给View添加其他事件监听
}
companion object {
private const val TAG = "MyClickListener"
private val timeout = 600 //双击间四百毫秒延时
init {
Log.i(TAG, "timeout is $timeout ")
}
}
}
手势识别
在 Android 开发当中,几乎所有的事件都会与用户进行交互,而我们用得的最多的就是手势了。
我们知道当我们触摸屏幕的时候,会产生很多事件,比如 down,move,up, fling 事件等等。一些简单的处理,我们可以直接重写 View 的 onTouchEvent 方法,根据 View 的 MotionEvent 事件进行处理。
而 Google 为了方便开发者方便接入,提供了几个默认处理类,那就是 GestureDetector 和 ScaleGestureDetector。
GestureDetector这个类对外提供了两个接口和一个外部类
。
接口:OnGestureListener,OnDoubleTapListener
内部类:SimpleOnGestureListener,同时实现了 OnGestureListener,OnDoubleTapListener 接口,如果只想使用接口里面的某个方法,可以直接使用它,方便快捷。
讲解之前,我们向来看一下怎么使用
GestureDetector(Context context, GestureDetector.OnGestureListener listener)
GestureDetector 基本使用
第一步,初始化 GestureDetector
对象
mDetector = GestureDetectorCompat(this, MyGestureListener())
可以看到有两个参数,第一个参数 context,第二个参数 OnGestureListener,我们可以直接实现 OnGestureListener 接口,也可以直接使用 GestureDetector.SimpleOnGestureListener
private class MyGestureListener : GestureDetector.OnGestureListener {
private val TAG = "GestureDemoActivity"
override fun onShowPress(e: MotionEvent?) {
Log.d(TAG, "onShowPress: e is $e")
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
Log.d(TAG, "onSingleTapUp: e is $e")
return false
}
override fun onDown(event: MotionEvent): Boolean {
Log.d(TAG, "onDown: $event")
return true
}
override fun onFling(
event1: MotionEvent,
event2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
Log.d(TAG, "onFling: $event1 $event2")
return false
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
Log.d(TAG, "onScroll: distanceX is $distanceX,distanceY is $distanceY ")
return false
}
override fun onLongPress(e: MotionEvent?) {
Log.d(TAG, "onLongPress: e is $e")
}
}
第二步:设置双击监听
// 设置双击监听
mDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
override fun onDoubleTap(e: MotionEvent?): Boolean {
Log.d(TAG, "onDoubleTap: e is e")
return false
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
Log.d(TAG, "onDoubleTapEvent: e is e")
return false
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
Log.d(TAG, "onSingleTapConfirmed: e is e")
return false
}
})
最后,重写 Activity 或者 View 的 onTouchEvent ,将事件交给 mDetector 处理。
通常会有两种写法,第一种是如果手势处理器处理了,直接返回 true,进行消费。否则,进行默认处理
override fun onTouchEvent(event: MotionEvent): Boolean {
return if (mDetector.onTouchEvent(event)) {
true
} else {
super.onTouchEvent(event)
}
}
第二种写法是直接在 onTouchEvent 方法中,直接调用 mDetector.onTouchEvent(event)
方法
override fun onTouchEvent(event: MotionEvent): Boolean {
mDetector.onTouchEvent(event)
return super.onTouchEvent(event)
}
第二种写法,一般不会影响当前 View 或者 Activity 事件的传递,在开发当中,有时候为了减少一些触摸事件的冲突,经常这样写。
ScaleGestureDetector 这里暂时不展开描述了,写着写着,发现好多呀,一个周末就这样过去,贼快,觉得对你有帮助的,请来个三连,点赞,收藏,转发????。
小结
这篇文章,其实不难。主要是将 View 的事件分发机制,滑动冲突,以及开发当中经常用到的一些知识点,总结一下。
参考博客:
http://www.jianshu.com/p/e99b5e8bd67b
https://xujun.blog.csdn.net/article/details/52939127
https://github.com/gdutxiaoxu/TouchDemo