导语
事件分发是一个老生常谈的问题,理解事件分发机制,对于解决日常开发工作中遇到的滑动冲突问题,有着至关重要的作本,这里以阅读源码的方式,对事件分发机制做一个详细的讲解,希望阅读本文后,能对事件分发机制有个深入的理解,解决日常遇到的滑动冲突问题
源码阅读
从Activity开始
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java super实际是ViewGroup的dispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
下面就进入了View Group的流程
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//字段含义,ev事件是否已经被处理(被消费)
boolean handled = false;
//简化代码,Down事件来临时,置空mFirstTouchTarget
if(actionMasked == MotionEvent.ACTION_DOWN){
mFirstTouchTarget = null;
}
// Check for interception.
//第一步,确定是否需要拦截事件(检查条件:DOWN事件或者mFirstTouchTarget状态)
final boolean intercepted;
//mFirstTouchTarget字段含义:有子view能处理事件,
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//viewGroup 调用自身的onInterceptTouchEvent,决定是否对事件进行拦截,自己处理该事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//第二步,不需要拦截场景处理,分发DOWN事件
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!intercepted) {
//仅处理DOWN事件(由第一步得知,不拦截事件时,ViewGroupu要问问自己的子View能否处理事件)
if (actionMasked == MotionEvent.ACTION_DOWN) {
if (newTouchTarget == null && childrenCount != 0) {
//遍历所有子view
for (int i = childrenCount - 1; i >= 0; i--) {
//内部调用child.dispatchTouchEvent,询问子view是否需要消费事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//若消费事件,则 mFirstTouchTarget从此有了值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//局部变量,记录子View已经对事件进行过分发处理
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
//第三步真正事件分发,将事件分发给对应View
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//从第二步得知,没有子View处理事件,只能ViewGroup自己处理了,内部会调用自己的onTouchEvent 来决定是否消费事件
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//从第二步得知,有子view能处理,则交给子view
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//子view已经分发过事件(因为第二步中down事件调用过dispatchTransformedTouchEvent进行分发)
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//子View未分发过事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
//第四步,将事件处理结果告知上层
return handled;
}
从源码中可以得知,整体上,事件的分发是由Activity自顶向下逐级传播的,通过Activity的dispatchTouchEvent最终访问到DecorView的dispatchTouchEvent方法,
dispatchTouchEvent是事件分发的入口,这里要重点讲解,从上述代码中标注的内容来看,我们可以得知,任何一个事件,都是从Down事件开始的,从dispatchTouchEvent的源码中我们也可以了解到,有很多针对DOWN事件单独处理的逻辑,因此之类解读源码,也分量两个流程讲解,DOWN事件的流程与其他事件的流程
DOWN事件的流程
- 第一步:确定是否需要拦截事件 --理解:DOWN事件到来时,确认是否需要自己拦截处理,前置清空mFirstTouchTarget字段,调用自身的onInterceptTouchEvent来确认是否需要自己主动拦截处理该事件
- 第二步:DOWN事件分发 --若自身不需要拦截DOWN事件,则将事件分发给子VIEW,调用child.dispatchTouchEvent递归询问子view是否能处理该事件,若子view能处理,则记录mFirstTouchTarget,和alreadyDispatchedToNewTouchTarget --若自身拦截,则直接跳过第二步
- 第三步:真正事件分发,将事件分发给对应View --根据第二步的结果,已经得知事件是自己处理还是子View处理,自身处理最终会调用自身的onTouchEvent来处理事件,若是子VIEw处理,因DOWN事件的特殊性,在第二步已经调用了child.dispatchTouchEvent,第三步则根据第二步记录的alreadyDispatchedToNewTouchTarget 字段,直接返回已经处理的结果
- 第四步:无
其他事件的分发流程
- 第一步:确定是否需要拦截事件 --理解:根据mFirstTouchTarget字段,判断,若存在能处理事件的子VIEW,调用自身的onInterceptTouchEvent来确认是否需要自己主动拦截处理该事件
- 第二步:无
- 第三步:真正事件分发,将事件分发给对应View --根据DOWN事件时记录的结果,已经得知事件是自己处理还是子View处理,自身处理最终会调用自身的onTouchEvent来处理事件,若是子VIEw处理,则调用了child.dispatchTouchEvent
- 第四步:返回第三步得到的结果给上层。
需要重点理解几个字段 mFirstTouchTarget:在Down事件时记录,子view能否处理事件,能的话后面的新事件交个子view处理 alreadyDispatchedToNewTouchTarget:为了防止多次触发子view的dispatchTouchEvent,用于记录的一个成员变量
总结
在理解上述流程时,是不是发现了我们在处理滑动冲突时,经常会遇到的几个方法 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 结合源码阅读,我们可以对这几个函数有更深入的理解
/**
* 事件分发器:在ViewGroup中,dispatchTouchEvent 负责确定事件是自己处理还是子view处理,通知通过返回值告知父view,自己和子view能否处理该事件
* true:告知父view,事件已经被自己或者子view处理掉
* false :告知父view,事件自己和自己的子view都不能处理,交由父view处理
* 分发过程
* 1、通过事件拦截器,优先确认是否拦截事件让自己处理,若确认拦截则跳过步骤2
* 2、确认不拦截,则调用子view的事件事件分发器 ,让自view确认自己能否处理
* 3、根据1、2的结果,若子view确认自己能处理,则交由子view处理,不会调用自己的事件处理器,若子view不能处理,则调用自身的事件处理器
* 4、将处理结果告知父view
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("whqq", "父类dispatchTouchEvent" );
boolean result = super.dispatchTouchEvent(ev);
Log.e("whqq", "父类dispatchTouchEvent结果" + result);
return result;
}
/**
* 事件拦截器:仅用于拦截触摸事件(ACTION_DOWN)
* true:拦截当前触摸事件,传递给自己的 onTouchEvent 进行处理
* false:不拦截当前触摸事件,
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
Log.e("whqq", "父类onInterceptTouchEvent" );
boolean result = super.onInterceptTouchEvent(e);
Log.e("whqq", "父类onInterceptTouchEvent结果" + result);
return result;
}
/**
* 事件处理器:处理到来的事件
* true:事件被处理掉了,告诉父view需要处理了,父view也不会在调用onTouchEvent
* false: 事件我处理不了,父view会调用自己的onTouchEvent
*/
@Override
public boolean onTouchEvent(MotionEvent e) {
Log.e("whqq", "父类onTouchEvent" );
boolean result = super.onTouchEvent(e);
Log.e("whqq", "父类onTouchEvent结果" + result);
return result;
}
思考
- 在解决滑动冲突问题时,最应该复写的是onTouchEvent方法,去处理滑动事件,
- 若复写了onInterceptTouchEvent方法,也要注意,既然选择拦截了,就代表要么你已经确认要处理该事件在onTouchEvent中返回true,要么代表问题是因为事件交给子view处理引入了问题,才复写onInterceptTouchEvent事件传递给子view,不然都是错误复写
- 若复写了dispatchTouchEvent方法,记的调用super