为了写这篇文章,我反复的看了好几十遍源码。而且写的时候时间间隔比较长,有时候写着写着自己都混乱了,又去看一遍源码去分析,所以可能会重复的内容比较多也会稍微乱一点,不过我相信你跟着源码和这边文章一步一步走,应该还是会有收获的!
本片文章将会介绍,view事件是怎么传递的和分发的,以及点击滑动冲突产生的原因和解决办法。这些都会通过阅读源码解决~
一些基础的知识
MotionEvent
当手指接触屏幕时,会先触发ActionDown一次,然后会触发一次或多次ActionMove,最后触发一次ActionUp
事件分发,拦截,消费
这是三个方法分别在activity,viewGroup,View中的存在状况
本片文章也是一直围绕这三个方法做不同情况的解读
简陋事件分发图
这里堆叠的是一个个view,因为view都是一个个堆叠在屏幕上的
onTouch 和 onClick
首先看一段简单的代码,给button设置onTouch和onClick事件。我们可以知道,
当onTouch事件 return false时,onClick 会执行
当onTouch事件 return true时,onClick 不会执行
结果我们都知道,也知道是onTouch拦截了才不会执行onClick。
但是这两段简单的代码在源码中是怎么体现的呢?
因为Button属于一个View,所有我们直接进入view的源码,
看他的dispatchTouchEvent事件分发的方法,
可以知道第一个if判断的结果,是会影响第二个if语句的执行的
首先在第一个if看到了我们的onTouch事件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
我们把if条件里的判断拆解一下
1.li != null && li.mOnTouchListener != null
首先可以看到 li = mLisenterInfo,mLisenterInfo是什么呢?
我们回设置onTouch事件调用的setOnTouchListener方法,进入查看,可以看到getListenerInfo()
接着再进入getListenerInfo()查看
通过这两幅图,我们可以知道的是
li = mLisenterInfo != null,
li.mOnTouchListener !=null(在setOnTouchListener赋值了我们传入的值)
所以第一个条件是成立的
2.(mViewFlags & ENABLED_MASK) == ENABLED
这个条件不用做过多的解读,就是判断能不能点击
所以第二个条件是成立的
3.li.mOnTouchListener.onTouch(this, event)
这调用的方法就是我们给button设置的onTouch了
li.mOnTouchListener.onTouch(this, event) return false ---> result = false
return true ---> result = true
// 第二个if语句
if (!result && onTouchEvent(event)) { }
3.1 假设我们 return false,那么第一个if语句就失效了不能进入,result还是初始值fasle
所以我们会执行第二个if ---> 执行onTouchEvent(event),事件消费
接着进入performClick,最终可以看到我们的onClick方法的调用
3.2 假设我们 return true,那么第一个if语句就失效了能进入,result被赋值为true
所以我们不会执行第二个if ---> 也就不能执行事件消费onTouchEvent(event),也就不能执行到onClick方法了
再从事件分发流程的角度来看
首先进入viewGroup#dispatchTouchEvent,分析一个正常的Down事件
注意:if(!canceled && !intercepted){}
这一个if语句里面的代码块,全都是与事件分发相关的,可以说只要进入了一个if语句,就会执行事件分发,接下来也是对if语句里面的代码进行分析
题外话
这里我们先来看2注释中buildTouchDispatchChildList方法
它最后会执行到buildOrderedChildList
里面将所有childView按照Z轴的大小,从小到大排序,
最小的在最前面,最大的在后面
Z轴是怎么来的呢,我们知道一个layout布局里面的所有View都是一个个叠加上去的就像这样,所以最底层的Z轴越小,越排在列表的前面。所以遍历的时候,也是从最后一个拿的
再看一下4注释中isTransformedTouchPointInView方法
比如你点击的是图中的小圆圈,当他遍历button1时,就会根据你点击的坐标和button1的区域做比较,看看是不是在自己的范围内,不是的话continue继续遍历下一个childview
再看一下4注释下面的方法newTouchTarget = getTouchTarget(child);
题外话结束
继续往下走的话,会进入到dispatchTransformedTouchEvent
因为child != null 进入else语句,接着就会调用child.dispatchTouchEvent(transformedEvent)
在上述案例中,button就是child,所以事件就这样分发给了button去做后续操作
button中没有重写dispatchTouchEvent,所以就进入View的dispatchTouchEvent
也就是回到我们一开始分析的结果了
可以看到,如果button处理或者消费事件或者在onTouch返回true(也算是处理),就会返回true。进而child.dispatchTouchEvent(transformedEvent)的结果就是true
接着回到之前的方法,if语句会被命中,接着会进入
而if语句最后会break掉,就直接退出了本次for循环了,本次事件就被button处理了,也不会被其他或者父view获取了,接着就会开始下一个view或者viewgroup了的时间分发了。
另外这里有两个红框的语句,会得到三个条件,特别注意一下
newTouchTarget = mFirstTouchTarget != null
mFirstTouchTarget.next = null
alreadyDispatchedToNewTouchTarget = true
这里把之前的代码折叠起来了,为了方便看。
最后的语句因为上面的上面的条件而不会命中,
最后整个if (!canceled && !intercepted)
里的代码块就结束了
接着往下走,因为mFirstTouchTarget != null 所以我们来看else语句的代码块
最后将handled的结果return,dispatchTouchEvent方法结束
这里Down事件结束
滑动冲突
上面只是正常的down事件分发
接下来用这个例子来看一下有冲突的事件分发来分析一下down和move事件
先介绍一下情况,布局是这样的:
自定义了一个BadViewpager,里面放着一个listview,listview里面有很多item,超过一屏幕
所以这里viewpager是父view,listview就是子view
BadViewpager正常情况下是可以左右滑动的
listview正常情况下是可以上下滑动的
BadViewpager,重写了onInterceptTouchEvent
拦截事件的方法,并且返回true(为了制造冲突)
如果 BadViewpager的onInterceptTouchEvent返回ture,拦截事件
此时viewpager是可以左右滑动的,但是listview不能上下滑动
也就是说事件分发到viewpager就被拦截了,让我们来看看viewpager是怎么拦截事件,并且自己消费事件的。
我们从头开始,回到ViewGroup的dispatchTouchEvent
注意:现在是ACTION_DOWN事件
因为在viewpager重写了onInterceptTouchEvent方法,导致intercepted的为true
我们知道if (!canceled && !intercepted) {}
是将事件分发给子View的关键代码块
intercepted是true 就表示if语句不能进入,就不能将事件分发给子View(也就是listview)
而且mFirstTouchTarget是在if (!canceled && !intercepted) {}
里面赋值的,所以往下走的话
注意这里传入的child是null,因为没有取消事件所以canceled为false
所以进入dispatchTransformedTouchEvent后我们可以看到
这里直接调用了自己的dispatchTouchEvent,就把事件分发给自己的
这里ACTION_DOWN事件就结束了
如果 BadViewpager的onInterceptTouchEvent返回false,不拦截事件
此时viewpager是不可以左右滑动的,但是listview能上下滑动
ACTION_DOWN事件流程和上面button的事件分发情况是一样的,就不分析了。
所以这里是分发了一次ACTION_DOWN事件后,
再次执行dispatchTouchEvent分发ACTION_MOVE。
所以有几个条件要注意
newTouchTarget = mFirstTouchTarget != null
mFirstTouchTarget.next = null
alreadyDispatchedToNewTouchTarget = false
(注意这里有不同)
因为每次执行dispatchTouchEvent,
alreadyDispatchedToNewTouchTarget 都会被重置
而alreadyDispatchedToNewTouchTarget 字段只有在分发子view时才会被赋值为true
但是根据下图的判断,在move事件中是不会执行下面的语句的
这里ACTION_MOVE事件
所以我们接分析下面的else语句
进入dispatchTransformedTouchEvent我们可以看到
这里ACTION_MOVE事件结束
内部拦截和外部拦截
先来看一下内部拦截
注意内部拦截是子view和父view都要进行处理的
在子view(listview)中
在父view(viewpager)中
首先来看一下getParent().requestDisallowInterceptTouchEvent()
传入true时 mGroupFlags | FLAG_DISALLOW_INTERCEPT
传入false时 mGroupFlags & FLAG_DISALLOW_INTERCEPT
所以到dispatchTouchEvent时(mGroupFlags & FLAG_DISALLOW_INTERCEPT)
传入true时,运算结果就是 !=0,disallowIntercept为true,intercepted为false,后续事件会正常分发
传入false时,运算结果就是 =0,disallowIntercept为false,intercepted根据onInterceptTouchEvent情况定
接下来再分析一下,为什么要再父view做处理,而不是直接返回true就行了
最简单的:不做处理返回true 子view就根本不会接收到事件。内部拦截的代码都不会执行。我认为这只是其中一个原因
根据这个案例来说,viewgroup分发事件给listview,listview也是一个ViewGroup。
所以它也会走ViewGroup的dispatchTouchEvent,这个时候问题就来了。
在分发ACTION_DOWN,会执行一个重置方法
这边将mGroupFlags 做了运算
再回到dispatchTouchEvent方法中时,mGroup又做了运算,
所以最终的值就是这样一个操作
mGroupFlags & ~FLAG_DISALLOW_INTERCEPT &FLAG_DISALLOW_INTERCEPT
,导致这个值肯定为0
所以disallowIntercept为false,直接进入下面的if语句
此时如果没在父view对onInterceptTouchEvent的down事件做处理的话,返回true
那么intercepted就会为true
cancel事件的产生
还是继续用这个案例,还是用内部拦截去分析,当down事件结束后,我们会进入到move事件
首先move事件进来时,是listview拿着这个事件,他会执行他自己的这个方法
由于上面知道传入false会导致disallowIntercept为false,intercepted根据onInterceptTouchEvent情况定
而此时的ViewGroup返回是true,所以intercepted为true,就会导致如下的执行
执行dispatchTransformedTouchEvent 并且下面会对mFirstTouchTarget赋值
前面已经分析过了,next就是null,所以这里是将mFirstTouchTarget赋值为null
进入dispatchTransformedTouchEvent查看一下,
我们可以知道,这里是取消掉子view的事件的
这个move事件执行完后,接着还是move事件,因为他是触发多次的,
这里就会直接走mFirstTouchTarget == null 的判定了,这时就是父view拿到事件了
所以就可以从listview的上下滑动转换到viewpager的左右滑动
结尾
外部拦截的流程就不分析了,分析下来其实和cancel事件产生的流程大同小异,就不做重复了。