之前有写过一篇简单的博客
解决滑动冲突问题
(原创)巧妙解决ViewPager和ScrollView冲突_Android_xiong_st的博客-CSDN博客
今天对冲突背后的事件分发机制,做一个详细的介绍
下面开始!
Android的事件分发机制相关的类:
public boolean dispatchTouchEvent(event):用于进行点击事件的分发
public boolean onInterceptTouchEvent(event):用于进行点击事件的拦截
public boolean onTouchEvent(event):用于处理点击事件
三个函数的参数均为even,即上面所说的3种类型的输入事件,返回值均为boolean 类型
上面的三种方法的调用关系大致可以用下面的伪代码来描述
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;//事件是否被消费
if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
}else{
consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
}
return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
}
一、事件分发的执行顺序流程图(默认情况下,不考虑事件拦截和处理)
如上图,事件的分发顺序由1开始依次分发到8。
事件分发的规则,隧道式下发。
在ViewGroup中如果有N个子控件可以处理该事件
(比如帧布局,很多子控件叠层在一起,那么这些叠层在一起的子控件都可以处理该布局上的触摸事件,
除非触摸事件发生在子控件的范围外),
那么最开始会分发给最后一个子控件,然后依次往前分发,如上图。
如果其中一个子控件又是一个ViewGroup,会先分发给该ViewGroup下的子控件,
按同样的规则依次分发,子控件处理完成后,再继续分发给同级的下一个View,
如上图(第二个View下的子控件全部分发完成后,才分发给第一个View)。
我们可以把事件分发的过程理解成一个二叉树的遍历过程。
dispatchTouchEvent()返回值详解:
dispatchTouchEvent()直接返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,
如果直接返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。
1、直接返回true:表示终止整个事件链的传递,后续事件(ACTION_MOVE、ACTION_UP)会再传递。
比如上图中第二个View(红色边框表示的ViewGroup),如果该ViewGroup对应的dispatchTouchEvent()方法返回true,
则事件不在往后分发,后面的控件也不会对事件进行处理,
第二个View的dispatchTouchEvent()成为此次事件分发链中最后被执行的一个方法(在这之前已经分发和处理了事件的控件不会受影响)。
第二个View的dispatchTouchEvent()也会收到后续事件(ACTION_MOVE、ACTION_UP)
2、直接返回false:表示停止当前链路的事件下发,但是不停止整个事件链的传递。也接受不到后续事件了。
比如按下上图中的第二个View(红色边框表示的ViewGroup),如果该ViewGroup对应的dispatchTouchEvent()方法返回false,
则事件在该事件链上停止下发,即5、6、7对应的控件(黄色边框表示的子View),不再接收到分发的事件,
也不能对事件进行处理。但是8对应的“第一个View”(绿色边框表示的子View),仍然可以接受到父控件分发下来的action_down事件并且进行处理,
因为8对应的“第一个View”不在“第二个View”所处的事件链中,他们是同级关系,因此不受“第二个View”的停止分发影响。
但是8对应的“第一个View”再也收不到后续事件ACTION_UP、ACTION_MOVE了。所以其实受到的影响就是只接受到了一次的down事件。
注意:以上两种直接返回true和false的情况,View自身也不会再调用拦截和处理的方法了
因为拦截和处理都是在super.dispatchTouchEvent()里面做的,做完再返回结果
相当于View自身也没有事件拦截和处理的资格了
3、(默认的情况)返回super.dispatchTouchEvent():继续分发,表示接受这个事件,此时又分三种情况:
1)当前View是一个ViewGroup,那么继续执行该ViewGroup的onInterceptTouchEvent()拦截方法进行拦截,根据拦截方法的返回值来决定后续动作。
2)当前View不是一个ViewGroup(比如TextView,不存在子控件),那么直接调用当前控件的onTouchEvent()方法来处理事件,并且根据处理事件方法的返回值来决定后续动作。
3)当前控件是一个Activity,那么如果当前Activity没有子控件(当触摸事件发生在最外层布局之外时,只有activity能处理该事 件,即activity不存在子控件)时,
直接调用本身的onTouchEvent()方法进行处理,否则分发给下一个子控件(即activity包含的 最外层布局对象)。
二、事件拦截流程图
事件拦截只会发生在ViewGroup中,Activity和View没有事件拦截这个方法。
事件拦截方法只存在于容器布局中,即ViewGroup。在ViewGroup中,事件拦截是在事件分发方法后被调用(前提是事件分发方法返回super)。
onInterceptTouchEvent()返回值详解:
1)返回true:表示拦截该事件,事件不在往下分发,
该ViewGroup的所有子控件都无法接收到事件(即子控件的dispatchTouchEvent()方法不会被调用),并且直接调用ViewGroup本身的onTouchEvent();
2)返回false:表示不对事件进行拦截,继续往下分发,
再根据子控件的事件处理结果决定是否调用自己的onTouchEvent()方法(如果子控件终止了分发或者消费了事件,都将导致ViewGroup的onTouchEvent()方法无法被调用,如果子控件没有消费事件,则调用自身的onTouchEvent()方法)。
3)返回super.onInterceptTouchEvent():如果继承的是ViewGroup,效果和返回false一样,我们可以进去看到源码,ViewGroup.onInterceptTouchEvent()返回的就是false。
当然,一些控件会做修改,比如我们继承ViewPager,ViewPager的onInterceptTouchEvent()就在自己内部做了处理,这时候就需要我们自己根据实际业务来决定onInterceptTouchEvent()的返回值了
三、事件处理顺序流程图
事件处理的规则,默认情况下(假设不存在消费的情况),子View先依次处理完毕,父控件再处理事件的一个过程。如上图。
onTouchEvent()返回值详解:
1)返回true:表示消费事件,即表示这个事件交由自己来处理,还未执行事件处理方法的控件将不再调用事件处理方法。事件链到此结束。
2)返回false:表示透传事件,即把这个事件交由其他控件来处理,这样事件链会继续传递,直到有一个控件消费该事件或交由最上层控件来处理(即activity)。
3)返回super.onTouchEvent():根据这个View有没有设置监听来决定返回值,如果设置了一些监听(比如click监听),并且符合了该监听的条件则返回true,否则返回false。
四、最后,是事件分发解决方式
假设有父布局A,里面嵌套一个子布局B,二者发生了滑动冲突,解决方案有两个:
1、外部解决,从自定义父布局开始解决
在父布局的onInterceptTouchEvent 拦截方法里,判断该子布局滑动的时候,把这个拦截方法的返回值返回false,意为不拦截,比如
注意:这里只是一个示例,可以看到没有返回true的情况,因为这里是拿一个Scrollview来示例。Scrollview在自己的onInterceptTouchEvent()里面做了处理,如果我们继承的是ViewGroup,就要根据实际业务自己决定什么情况下返回true了。
2、内部解决,从自定义子布局开始解决
在子布局的dispatchTouchEvent拦截方法里,判断该子布局滑动的时候,请求父布局不要拦截自己,比如
另外在父布局的拦截方法里还要处理以下ACTION_DOWN事件
在ACTION_DOWN时返回false
确保子View拿到action_down事件,然后调用requestDisallowInterceptTouchEvent(true)
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
内部拦截的处理方式我写了个小demo:HorizontalScrollView嵌套一个ListView
xml如下:
两个自定义控件如下:
public class MyHorizontalScrollView extends HorizontalScrollView {
public MyHorizontalScrollView(Context context) {
super(context);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MyHorizontalScrollView", "父dispatchTouchEvent:"+ev.getAction());
// return false;
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("MyHorizontalScrollView", "父onInterceptTouchEvent:"+ev.getAction());
if(ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MyHorizontalScrollView", "父onTouchEvent:"+event.getAction());
return super.onTouchEvent(event);
}
}
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MyHorizontalScrollView", "子dispatchTouchEvent:" + ev.getAction());
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//横向偏移大,判定为横向滑动,交给父容器处理
if (Math.abs(deltaX) > Math.abs(deltaY)) {
//父容器恢复事件拦截,事件交给父容器处理
Log.d("MyHorizontalScrollView", "交给父布局");
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
// return false;
boolean b = super.dispatchTouchEvent(ev);
return b;
}
//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MyHorizontalScrollView", "子onTouchEvent:" + event.getAction());
return super.onTouchEvent(event);
}
}
写在最后:
在开发的过程中,除了要解决问题外
还要多关注问题背后的事情
比如问题产生的根本原因
系统的一些机制等等
只有这样,才能进步啊~
加油!
最后贴一些参考博客
Android事件分发机制_你的坚定的博客-CSDN博客_android 事件分发