Android开发中,事件冲突是老生常谈的问题,每次碰到事件冲突都觉得头大,一顿操作后,发现问题竟然解决了。通常事件冲突有两种解决方案
- 外部拦截法:通过父容器去拦截事件,如果父容器需要处理事件就拦截,否则就不拦截,从而达到解决事件冲突问题
- 内部拦截法:父容器不拦截任何事件,事件都传递给子元素去处理, 如果子元素需要此事件则直接处理掉,否则还给父容器去处理。这种处理方式需要配合requestDisallowInterceptTouchEvent才能正常工作,这种方案其实有个坑
事件分发流程
Activity dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Android 中我们知道window其实就是PhoneWindow, 所以其实就是调用PhoneWindow的superDispatchTouchEvent
PhoneWindow superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor其实就是我们非常熟悉的DecorView了
DecorView superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
所以到这里就清晰了,就是调用了ViewGroup的dispatchTouchEvent。ViewGroup 事件分发流程图网上一抓一大把,所以我们从源码角度去分析ViewGroup事件到底是如何分发的
ViewGroup dispatchTouchEvent
一个事件系列始于down事件,一系列move事件,终于up事件
-
down事件 从正常流程来分析
disallowIntercept在down事件值一定为false,为什么?往上面我们看到down事件里调用了 resetTouchState(),这个方法其实就是重置状态
private void resetTouchState() { ... mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; ... }这个方法里面重置了mGroupFlags的值,所以这也是为啥说disallowIntercept值为false原因 因此流程就来到了dispatchTouchEvent --> onInterceptTouchEvent
假设ViewGroup 不去拦截事件,intercepted = false
接下来我们看到这么一个方法块TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { ... }正常情况下canceled=false,再看看这个方法块里面做了些什么事情?
1、对ViewGroup 子View 根据Z轴做排序 buildTouchDispatchChildList()
2、由上往下找到这个触摸事件可以给哪个子元素去处理... if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { continue; } ...3、找到子元素之后
dispatchTransformedTouchEvent() 里面有个关键性判断
... if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); } ...由上面分析得到,此时child就是ViewGroup上面能处理这个down 事件的子View, 所以child不为空 所以就调用了View 的dispatchTouchEvent, 这也就验证了我们开篇图的事件流程。
4、假设我们的子View处理了事件 那么view dispatchTouchEvent返会了true, 上面图最后两行代码得出 mFirstTouchTarget 就是我们此次处理事件子View, 然后就退出循环, 这次事件结束 5、假设我们的子View不处理事件, 那么继续往上找父容器去处理, 这也是为何上面会对ViewGroup子View进行排序原因, -
move事件
1、由down事件分析得制,此时mFirstTouchTarget就是子View,重复down事件步骤,不同点在于不会再去寻找子View再去分发,事件由处理down 事件子View持有2、由于mFirstTouchTarget 不为空, 参照down事件步骤3 所以这次move事件还是由子View去处理
-
up事件 类似move 事件流程
案例
使用外部拦截或内部拦截来分析事件流程
- 外部拦截法代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
if(condication){
return false;
}
}
return true;
}
由上面事件分析流程我们可以看出,当父容器拦截事件返回true后,down事件中, !canceled && !intercepted 条件是不成立的,而此时mFirstTouchTarget = null, 因此直接就调用了dispatchTransformedTouchEvent(), 所以这时候事件直接由父容器处理。所以这个条件就决定了事件由父容器处理还是由子元素处理,这也是为何外部拦截法能处理事件冲突原因。
- 内部拦截法代码
父容器
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
子容器
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mAutoPlayAble) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
if(condication){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
}
return super.dispatchTouchEvent(ev);
}
接着分析为何这种方式也可以直接处理事件冲突
1、down事件流程还是正常流程,此时事件由子View处理
2、move 事件中,如果符合子View move 事件, 那么disallowIntercept会被置为false 由于父容器onInterceptTouchEvent 返回true, 所以此时intercepted = true
3、
由于ihntercepted=true, 所以alreadyDispatchedToNewTouchTarget=false, 因此cancelChild = true, 再次回到dispatchTransformedTouchEvent
这代码块 event事件重置为ACTION_CALCEL, 由于child 还是那个子View, 所以这个事件还是给子View处理,只不过这时候事件类型是ACTION_CALCEL, 回到步骤3 由于cancelChild为true, 所以这时候mFirstTouchTarget = null
4、当第二个move事件进来后, 如何把事件从子View 还给父View? 接着分析。
5、由于第一个move事件把mFirstTouchTarget = null, 因此这时候intercepted = true, 直接回到这里
6、还是回到dispatchTransformedTouchEvent 这个方法,还是回到了最开始down事件一样流程
super.dispatchTouchEvent 所以事件又回到了父容器。
因此这种内部拦截法也能解决事件冲突, 唯一的坑就是down 事件中 父容器onInterceptTouchEvent 要返回false
Android 事件分发采用责任链模式,一层层往下传递,不处理就一层层往上传递。