上周面试被问到这个自己觉得简单不会问的问题[哭],回答逻辑混乱,这里看一复习一遍。。
参考:http://blog.csdn.net/lmj623565791/article/details/38960443
参考文章中已经描述的很清楚,这里仅记录下要点加深映像。
View事件处理
首先进入dispatchTouchEvent方法
1.View是在dispatchTouchEvent方法中处理touch事件的,并且是由ViewGroup在dispatchTransformedTouchEvent(..., View child, ...)方法中调用。
2.dispatchTouchEvent中优先判断是否设置了touchListener,如果设置了并且onTouch方法返回true,则不执行后续的onTouchEvent方法,且dispatchTouchEvent方法返回true表当前view要消费这个事件。 如果没有设置onTouchListener或者onTouch方法返回false,则调用view自身的onTouchEvent方法,并且dispatchTouchEvent方法返回onTouchEvent方法的返回值。
进入onTouchEvent方法
3.一开始直接判断当前view如果是disable的并且是clickable或longClickable或contentClickable的,直接返回true.表示当前view消费事件但是并不执行任务操作。
4.接着判断如果touchDelegate不为空并且onTouch方法返回true,则onTouchEvent直接返回true.
5.然后在判断如果当前view是clickable或者longClickable或者contentClickable的,直接返回true,否则直接返回false。这说明不管当前view有没有设置各种click的listener,只要是各种clickable的就会消费当前事件。
6.ACTION_DOWN时,先看代码
a.首先会判断当前view是否在一个可以滚动的viewgroup中,isInScrollingContainer方法会递归往父级找,直到找到某一级viewgroup的shouldDelayChildPressedState方法返回true(ViewGroup类中此方法默认返回true),表示需要延迟一下child的pressed状态设置(通过mPendingCheckForTap),然后通过post一个CheckForTap的runnable来延迟设置pressed状态(如注释描述,这样做为了避免当前用户是要进行scroll操作而不是click),并且这里会把当前状态设置为prepressed状态。 否则如果不在一个scrollingContainer中则直接改变当前view的状态为pressed并且调用checkForLongClick方法post一个CheckForLongPress的runnable对象。
b.看下CheckForTab
首先把prepressed状态清除,状态调用setPressed方法设置为pressed状态,其中会根据press的状态改变设置drawable,并且会调用dispatchSetPressed(boolean pressed)方法。然后在调checkForLongClick方法post一个longClick处理的runnable,并且这里会用longClick的生效时间longPressTimeout500减掉从down事件到现在delay的tapTimeout100.
c.在看CheckForLongPress
从down开始过了500ms后checkForLongPress还没有被remove掉则会调用处理longClick事件。如果当前是pressed状态并且parent为不空,则会调用performLongClick方法,其中会调用到onLongClickListener的onLongClick,并且如果onLongClick返回true则mHasPerformedLongPress会被设置为true.这个变量在up时会用来判断是否需要执行click.
7.的回到onTouchEvent的ACTION_MOVE
这里仅是判断当前move的位置是否在当前view内,如果不在则调用removeTapCallback,清除prepressed状态和remove掉checkForTap的runnable,不管从down到现在是否已经超过100ms(如果超过了也不会有影响)。如果当前是pressed状态则把checkForLongPress也remove掉并且取消pressed状态。
经过上面的两个remove之后,如果从down到现在还不到500,那么根据Up中开头上面的判断,当前即不会是prepressed也不会是pressed状态,所以手指只要move出当前view的范围过,那么click事件就不会在触发了。如果已经超过500,longClick已经触发,那么move只会影响click的触发,不管longClickListener是否存在或者onLongClick返回值是什么。
8.然后是CANCEL事件
正常情况下不会收到cancel事件,而且也只会在当前view之前已经消费了donw事件后才有可能收到,有时候父级控件中有可能需要阻断事件的传递自己消费(即使之前的donw事件已经由子view消费),这时候父级控件就会给之前消费了down事件的子view一个cancel事件让子view恢复之前事件设置的各种状态。
9.最后是UP事件
a.如果当前是prepressed状态(100ms前),直接设置为pressed.
b.如果mHasPerformedLongPress为false,可能是没有到达500ms或者到达了但是没有LongClickListener或onLongClick返回false.则会处理click事件。
c.通过UnsetPressedState来把当前view的pressed状态设置为false.这里如果是还不到100ms会delayPost 64ms清除的操作,猜测是因为从down到up如果还不到100ms(checkForTab没有执行),就在up中手动设置了pressed为true,为了让view从pressed到unpressed有一小段时间间隔(如为了drawable展示切换能被看到)才加的。
d.最后调用removeTapCallback()把checkForTap检查remove掉。这里之前老是想为什么要放到最后执行,万一在onClick中执行时间过长,在执行到这句代码前checkForTap的runnable初始执行了怎么办,后来想起来都是在主线程执行的,不会存在这种问题,放到最前和最后都是一样的。
ViewGroup事件处理
首先入口也是dispatchTouchEvent方法(重写了View的方法),其次是在PhoneWindow中被调用的
这里不在往上查是哪儿调的phonewindow,着重分析ViewGroup的dispatchTouchEvent,代码很长,分块讲解。
1.如果当前是DOWN事件,进行初始化设置。
如注释描述,可能由于app的切换,ANR或者其它状态变化的原因导致framework把上一次手势的UP或者CANCEl事件丢掉了,从而导致在上一次手势中设置的各种状态没有处理完,如果不做初始化会使得这次手势处理混乱。
这个方法中的mFirstTouchTarget代表的是上次手势流程中消费所有touch事件的view。这里初始化会通过dispatchTransformedTouchEvent方法给这个view分发一个cancel事件来让它自己初始化它由上一次手势设置的各种状态。然后通过clearTouchTargets把它回收掉并置空(后面需要重新找到消费这次手势流程的touchTarget)。
2.判断是否需要intercept事件传递。
a.首先由于上一步的原因,这里外层if中的两个条件肯定最多只会有一个为true.
b.如果两个都为false,说明当前不是初始的down事件而且之前的down事件也没有找到需要消费的target,那么直接就在else中把intercepted设为true代表直接阻断不用在去子view中找需要消费事件的view.
c.如果当前为down事件(当前还没有消费事件的mFirstTouchTarget)或者mFirstTouchTarget不为空(说明当前不是down事件,且之前在down时已经找到了消费这轮手势的target),就需要判断下是否需要阻断事件。判断是根据是否可以阻断(disallowIntercept为true代表不能阻断)和是否需要阻断(onInterceptTouchEvent返回true代表需要阻断,默认false)来决定的。
刚刚查了下原来可以设置为markdown编辑器的。。无知如我。。但是只能新建文章才能用。。这篇就将就下继续贴图吧。。
3.查找消费手势的子view
仅在当前事件为down并且没有被intercepted时才会执行查找操作(多指触摸不分析了。。)。各种判断不贴了
如注释描述,从前到后扫描子view,找出可以接收事件的子view。这里有两个接口会影响扫描子view时的顺序,一个是buildTouchDispatchChildList(),通过构造一个接收事件分发的子view列表,它里面是根据子view中的z(和x,y对应)属性和自定义的drawingorder来构造的。另外一种影响顺序的方式就是自定义的drawingorder了。
遍历子view列表,判断每个子view是否可以接收触摸事件,看不到的view或者正在执行动画的子view不能接收。并且判断触摸点是否在子view的范围内。
接着通过dispatchTransformedTouchEvent方法,把事件通过子view的dispatchTouchEvent方法传递下去,让子view自己决定是否需要消费事件。如果子view消费了事件就会把child通过addTouchTarget方法设置为之前见过的mFirstTouchTarget(重申这里不考虑多指触摸的情况)。然后直接break跳出查找。到这就找到新的需要消费downg事件的newTouchTarget.
4.其它
1.mFirstTouchTarget == null有三种情况。一种是down事件时查找消费事件的子view没找到,这时直接调dispatchTransformedTouchEvent,因为传的child为空,这时会直接调super.dispatchTouchEvent方法,处理的方式就和上面讲的View的事件处理一样了。另一种情况是还是在down事件但是并没有查找子view(被intercepted了)。第三种是当前不是down事件并且在之前的down事件中没有消费事件的子view(可能是没找到也可能是viewgroup自己处理了),这里能接收到非down事件说明之前的down事件中这个viewgroup的dispatchtouchevent方法肯定返回的true.
2.alreadyDispatchedToNewTouchTarget为true是之前down事件查找消费的子view时子view返回true.代表已经处理过down事件,这里就不用在执行一次了。
3.这里是处理两种情况的,一种是viewgroup想要intercept,所以会给子view分发一个cancel的事件。另一种是把当前事件(非down事件)分发给子view。这里我有点疑惑的是第一种情况下,viewgroup想intercept,在给子view分发一个cancel事件后并没有在调用super.dispatchTouchEvent来自己处理当前事件。
总结
差不多就这样了。
1.首先入口都是dispatchTouchEvent方法。
2.viewgroup可以阻断,子view也可以通过requestDisallowInterceptTouchEvent方法设置不让parent阻断,并且这个设置在每次手势流程结束后(不管是UP还是Cancel)都会初始重置。
3.viewgroup如果阻断了子view消费事件会给子view一个cancel.
4.只有在down是viewgroup才会查找子view中需要消费事件的,如果之前已经找到就直接把事件分发给它。