在我们的代码中,事件是由 dispatchTouchEvent 负责分发,由 onInterceptTouchEvent 决定是否拦截,最后由 onTouchEvent 消费;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
这是设置点击事件的方法,那么setOnClickListener究竟是如何生效的?点进setOnClickListener源码:
通过源码发现只是将我们的处理逻辑传给了ListenerInfo中,正真的调用是在 View 中的 dispatchTouchEvent中实现的:
我们注意上面圈起来的 if 判断:
如果我们只设置了setOnClickListener,那么很明显进不去这个 if ,接着看下面的代码:
result 默认为false,中间是 && 所以会执行后面的 onTouchEvent:
onTouchEvent也就是上面说的事件消费者,我们的触摸事件分为以下几个:
点击事件的触摸流程是DOWN,MOVE,UP,直到UP之后才算完成点击,所以点击事件的调用肯定在ACTION_UP中:
我们往下看会看到这样的一句代码:
在setOnClickListener的源码中,我们就是把单击事件逻辑赋值给了ListenerInfo中的mOnClickListener,在performClick中 进行了调用;
这就完成了一次单击事件
根据上面的流程图,我们从Activity开始一层层分析源码找到事件冲突的根源,首先是Activity中的dispatchEvent:
Activity中通过getWindow 调用了 superDispatchTouchEvent ,getWindow 是获得了Window的对象,Window的实现类只有一个也就是 PhoneWindow,由Window的源码中的注释我们也能得到这一点;
接着看PhoneWindow的源码:
在PhoneWindow的superDispatchTouchEvent中调用了 mDecor的superDispatchTrackballEvent,这个mDecor也就是DecorView
接着看DecorView中的superDispatchTouchEvent方法:
接着点进去dispatchTouchEvent:
跳到了ViewGroup中的dispatchTouchEvent,先是进行了一系列检查的代码,这些不重要,接着往下看代码:
我们触摸事件肯定会先触发DOWN,我们假设现在的DOWN,进来这个if之后会清除一些target 和 state,接着往下走:
到这里,就和onInterceptTouchEvent连接上了,onInterceptTouchEvent也就是 事件拦截者,接着往下走:
注意这个if, newTouchTarget在初始化的时候是null,后面的childrenCount 是当前viewGroup中的子View数量,所以可以进去这个 if 判断:
我们看一下buildTouchDispatchChildList 这个方法:
这个方法主要是对view进行了排序,这里由一个 getZ 方法,下面画图解释一下:
假设现在一个Activity的布局是这样的,x轴 y轴都没有问题
Z轴实际上是这样的:
所以,根据源码中的逻辑,排序后的数组应该是这样的:
我们继续回到ViewGroup中的dispatchTouchEvent方法中:
拿到这个存放view的数组后开始循环,循环的第一个,应该是在最上层的子view
,拿到这个view之后 接着往下走会看到这样的代码:
canReceivePointerEvents 主要判断view 是否动画 是否可见
isTransformedTouchPointInView 判断触摸的区域是否是当前view
如果判断不通过,则 continue 继续拿下一个view,如果通过,则不走if 继续往下走:
这里可以看到 返回的target一般是不为null的:
到这里,循环就结束了,接着往下看:
这里有调到了dispatchTransformedTouchEvent方法,并且cancel是false,接着看dispatchTransformedTouchEvent方法:
传过来的child不为空则,调用child 也就是 子view的dispatchTouchEvent:
到这里为止,就和前面分析的单击事件源码流程对上了。
那么也就是说,布局中的一层层view,是从最上层的view开始处理事件,当前view不处理的话,再抛给它的父view,以此循环。
由上面的分析,我们只需要指定 view 或者 它的父view 是否需要拦截事件即可,以下场景为例,ScrollerView的中间嵌套一个ScrollerView:
那么它冲突的原因就是子View(也就是里面嵌套的ScrollerView)的事件被父View(最外层的ScrollerView)消费了;
那么我们可以根据条件修改子View拦截事件的时机,代码如下:
public class MyScrollView extends ScrollView {
float mLastX = 0;
float mLastY = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mLastX = ev.getX();
mLastY = ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float dx = x - mLastX;
float dy = y - mLastY;
Log.e("dx ->", dx +"");
Log.e("dy ->", dy +"");
if(dy < 0){//向下滑
//滑动到底部 停止拦截事件
if (getHeight() + getScrollY() == getChildAt(0).getHeight()) {
getParent().requestDisallowInterceptTouchEvent(false);
}
}else if(dy > 0){//向上滑
//滑动到顶部 停止拦截事件
if (getScrollY() == 0){
getParent().requestDisallowInterceptTouchEvent(false);
}
}else {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
}
以上代码只是简单的处理了冲突,子view 可以响应自己的事件,实际工作中的开发,需要根据业务逻辑,在各种条件中判断子View何时需要拦截事件。