虽然网上关于这一块的博文很多,但是找了很久都没有找到比较全面的分析,所以想自己也开始写一些博客,一来让自己加深印象,二来希望能够给大家多多少少带来一些帮助。好了废话不多说,直接进入主题。
Android事件分发图
如果用一张图来描述事件的流程走向的话,那么下面这张图可以说是比较全面的且通俗易懂。首先先解释下三个方法的意思,dispatchTouchEvent()–是否分发事件,onInterceptTouchEvent()–是否拦截事件,仅ViewGroup,onTouchEvent()–当事件触摸是否进行操作。
看完上图是否已经有一定的理解?在这里简单总结一下:
- Activity中有dispatchTouchEvent()及onTouchEvent()方法
- ViewGroup中有dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()方法。
- View中有dispatchTouchEvent()、onTouchEvent()方法。
- 如果不重写任何方法的情况下系统默认调用的父类的方法,事件的走向为Activity - ViewGroup - View - ViewGroup - Activity,最后由Activity自己消费掉。
- 如果任何类中重写dispatchTouchEvent()方法,return false,那么表示不继续向下分发,而是直接流向上一级的onTouchEvent()方法中,如果return true,那么表示自己消费,本次事件在此终结,不再向任何地方分发。
- ViewGroup中有一个onInterceptTouchEvent()方法,表示是否需要拦截子类的触摸事件,如果该方法中return true,那么表示该事件由ViewGroup自己消费,事件不会再向下传递,而是直接有ViewGroup的onTouchEvent()自己处理,如果return false,则表示不拦截,事件继续分发至子View中。
- 同样,首先流到子View时,需要先走子View的dispatchTouchEvent()方法,如果不重写则该事件交由子View的onTouchEvent()处理消费。
- 如果子View的onTouchEvent()重写返回true,那么表示该该事件由子View处理,事件就此终结,反之,如果不重写该方法或者返回false,那么事件继续流向上一级的onTouchEvent()处理,直到Activity终结。
以上就是事件分发的大致流向路线,看起来虽然复杂但其实结合图形分析的话,还是很容易理解的。
结合源码分析
当然了解到这里肯定还不够,以上只是事件的大致传递流程,基于不考虑触摸动作的情况下,那么我们结合源码继续分析,当触摸的动作不同的情况下,事件的走向又是什么样的呢?
在以上的所有类回调的所有方法中的参数会提供一个Event对象,该对象包含三种事件的动作,根据鼠标的动作,Event.getAction对应值如下:
- Event.ACTION_DOWN(鼠标点下去的那一瞬间调用)
- Event.ACTION_MOVE(当鼠标移动时)
- Event.ACTION_UP(当鼠标抬起时)
根据getAction判断鼠标的动作,当鼠标动作不一样时,事件走向又是怎么走的呢?先看源码我们再进行分析
dispatchTouchEvent()中ACTION_DOWN:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
dispatchTouchEvent()中ACTION_MOVE:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//...ACTION_DOWN
//...ACTIN_UP or ACTION_CANCEL
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
//....
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
dispatchTouchEvent()中ACTION_UP:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {...}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if(target ==null ){...}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
正常情况下,即我们上例整个代码的流程我们已经走完了:
1、ACTION_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用 mMotionTarget.dispatchTouchEvent
2、ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
3、ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
当然了在分发之前都会修改下坐标系统,把当前的x,y分别减去child.left 和 child.top ,然后传给child;
也就是说,三个动作根据返回的值来决定是否继续分发至下一级,如果继续分发,那就将此动作的坐标值作为参数传给自己的onInterceptTouchEvent(ev)方法。这里值得注意的是返回值的关键在 if(disallowIntercept || !onInterceptTouchEvent(ev))这一句代码,通过这一句代码判断是否需要进行拦截,局部boolean类型的变量disallowIntercept (是否驳回拦截,通过该类中的viewGroup.requestDisallowInterceptTouchEvent(boolean)方法进行赋值,后面继续讲)及onInterceptTouchEvent(ev)的返回值,默认是进入该if语句中的方法,并通过遍历得到子View并调用子View的dispatchTouchEvent(ev)方法将事件传递下去
事件拦截源码
上面事件传递到子View的dispatchTouchEvent(ev)方法的前提是当ViewGroup.onInterceptTouchEvent(ev)不拦截的时候(disallowIntercept的默认为false,不重新赋值的情况下)
如果当ViewGroup.onInterceptTouchEvent(ev)不一样呢,我们复写下面这一段代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_MOVE:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_UP:
//如果你觉得需要拦截
return true ;
}
return false;
}
默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。
原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null 。
好的,重点来了,当MOVE时return true,子View还想拿到MOVE和UP事件怎么办呢?方法很简单,我们回顾下一开始的ViewGroup中的dispatchTouchEvent(ev)–ACTION_DOWN中的源码:
- 在源码里如果不重写,该方法默认会找到被点击的子View。
- 并调用该子View的dispatchTouchEvent(ev)方法
- 如果子View想禁止ViewGroup在MOVE的时候拦截事件,那么机会来了,当调用子类的dispatchTouchEvent(ev)方法的时候,我们在子类的方法中添加这句代码getParent().requestDisallowInterceptTouchEvent(boolean)传入true即将ViewGroup中的ViewGroup的变量disallowIntercept的值 设置为true了,也就是驳回拦截,那么当ViewGroup再次MOVE和UP时,就直接忽略ViewGroup的拦截方法的返回值了,直接进入IF语句将事件往下传递。
- 当然,如果ViewGroup一开始DOWN时就直接反馈true,那么上面的方式是无效的
总结
事件分发的流程大致情况如上,本次重点想让大家理解是当ViewGroup的MOVE时事件被拦截了,View也可以获取事件的方法及原理。个人见解,如果有误,欢迎指正,谢谢!