一、事件分发简介
1. View 触摸事件
对于屏幕的点击,滑动,抬起等一系的动作,其实都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:
(1)ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
(2)ACTION_MOVE:手指在屏幕上移动时候产生该事件
(3)ACTION_UP:手指从屏幕上松开的瞬间产生该事件
(4)ACTION_CANCEL 当前 View 的手势被打断,后续不会再收到任何事件从 ACTION_DOWN 开始到 ACTION_UP/ACTION_CANCEL 结束我们称为一个事件序列正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面 3 种情况。
1.点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP
2.点击后再风骚的滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
3.某些情况下,我们可能会没有收到 ACTION_UP 事件,是收到 ACTION_CANCEL 事件。
ACTION_CANCEL 一般是指 ChildView 原先拥有事件处理权,后面由于某些原因,该处理权需要交回给上层去处理,ChildView便会收到ACTION_CANCEL事件。对于一些复位或者重置操作,我们应该在 ACTION_UP 和 ACTION_CANCEL 里面同时进行处理。
代码逻辑上是:上层判断之前交给ChildView的事件处理权需要收回来了,便会做事件的拦截处理,拦截时给ChildView发一个ACTION_CANCEL事件。
2. 主要方法
View 的事件分发机制主要涉及到以下几个方法:
- dispatchTouchEvent ,这个方法主要是用来分发事件的
- onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是 ViewGroup 才有这个方法,View 没有
onInterceptTouchEvent 这个方法) - onTouchEvent 这个方法主要是用来处理事件的
- requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true 表示父
View 不拦截事件,false 表示父 View 拦截事件
3.图解事件处理流程
- 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
- 事件从最上边的黑色虚线箭头开始,由 Activity 的 dispatchTouchEvent 进行分发
- 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super
的意思是调用父类实现。) - dispatchTouchEvent和
onTouchEvent两个如果方法返回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 再来开始这个事件的分发。
二、事件冲突解决
在开发当中,View 的滑动冲突时经常遇到的,比如 ViewPager 嵌套 ViewPager、ScrollView 嵌套 ViewPager、ViewPager嵌套RecyclerView和ScrollView嵌套RecyclerView等等,下面让我们一起来看看怎么解决。
1.常见的冲突情况
(1)第一种情况,滑动方向不同
(2)第二种情况,滑动方向相同
(3)第三种情况,上述两种情况的多级嵌套
2.如何解决
看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突。那既然同一时刻只能由某一个 View 或者 ViewGroup 消费拦截,那我们就只需要决定在某个时刻由这个 View 或者 ViewGroup 拦截事件,另外的 某个时刻由 另外一个 View 或者 ViewGroup 拦截事件,不就 OK了吗?
这里针对第一种冲突情况介绍一下解决方法:
(1)第一种解决方法
从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不要的时候返回false,代码大概如下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//当前滑动x位置
final float x = ev.getX();
//当前滑动y位置
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//按下时x位置
mDownPosX = x;
//按下时y位置
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算x滑动距离
final float deltaX = Math.abs(x - mDownPosX);
//计算y滑动距离
final float deltaY = Math.abs(y - mDownPosY);
// 这里可以判断是否是左右滑动来控制父View是否拦截此事件
if (deltaX > deltaY) {
//若x方向的滑动距离大于y方向的滑动距离则此次滑动判断为左右滑动,父View可以拦截此事件消费
return true;
}else {
// 这一步可以不写,父View 返回false和return super.onInterceptTouchEvent(ev)的效果是一样的
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
(2)第二种解决方法
从子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事件,不让父View拦截事件
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);
// 判断是否是上下滑动,如是上下滑动则在子View消费事件
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);
}
(3)项目中遇到滑动冲突的解决方法
如下面这张设计图,想要的效果是两个页面来回切换,页面的Item可以侧滑拉出删除按钮,这里的布局是由ViewPager+Fragment+RecyclerView设计的,但是做出来的效果,Recyclerview侧滑和ViewPager侧滑会产生冲突,导致RecyclerView的Item侧滑卡顿甚至滑不出来。
这种情况的解决思路可以从内部子View即RecyclerView入手,首先判断是否在RecyclerView中进行滑动,在判断删除按钮是否已经滑出,进而将滑动事件交由父ViewGroup处理。
主要解决代码如下:
/**
* 重写RecyclerView的dispatchTouchEvent 解决ViewPager滑动冲突 导致侧滑删除按钮滑动卡顿的问题
*
* @param ev MotionEvent
* @return boolean
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
initViewParent();
if (viewParent == null) {
return super.dispatchTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
//recyclerview中按下后交由recyclerview处理
viewParent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getX();
int endY = (int) ev.getY();
int scrollX = endX - startX;
int disX = Math.abs(scrollX);
int disY = Math.abs(endY - startY);
if (disX < disY) {
//如果是横向滑动,告知父布局不进行事件拦截,交由子布局消费, requestDisallowInterceptTouchEvent(true)
viewParent.requestDisallowInterceptTouchEvent(canScrollHorizontally(startX - endX));
}
//这里判断左滑且删除按钮已关闭的情况 滑动事件交由父布局处理
if (scrollX > 0 && mFlingView.getScrollX() == 0) {
viewParent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
viewParent.requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 初始化viewParent
*/
private void initViewParent() {
viewParent = getViewParent();
}
/**
* 递归查找ViewPager父布局 没有就返回空
*
* @return ViewParent
*/
private ViewParent getViewParent() {
if (viewParent == null) {
viewParent = getParent();
} else {
viewParent = viewParent.getParent();
}
if (viewParent == null) {
return null;
}
if (viewParent instanceof ViewPager) {
return viewParent;
} else {
return getParent();
}
}
参考:
View 事件分发机制