android中事件分发机制是android中常见的问题,一般大家都知道view的分发事件是从view的Viewgroup(Parent)#dispatchTouchEvent到Viewgroup(Parent)#onInterceptTouchEvent再到View#dispatchTouchEvent,然后到view的onTouchEvent,最后又回到了Viewgroup(Parent)#onTouchEvent。如果大家记不住方法名,可以直接说先是parent的分发到拦截再到view的分发,再到view的消费,最后到parent的消费
这样回答肯定是很浅显的,因为没有说出是否拦截、是否分发、是否消费的各种条件,没有涉及到各种action的分发情况,上面说的默认分发只是针对action_down的,因为view/viewgroup各种super调用都是不进行分发、拦截、消费的,所以在没找到处理touch事件的view时候,是一直往上层view传递的,一直传到activity里面,下面我们再来整理一下:
如果viewgroup不进行分发,那么action_down、action_move和action_up只会执行到viewgroup的dispatchTouchEvent,不分发的条件是dispatchTouchEvent直接返回true或false,true和false的区别是true会执行action_down、action_move和action_up,而如果直接返回false只会执行到action_down。并且后续的viewgroup的onInterceptTouchEvent后续方法都不会被执行到。
关于为什么view/Viewgroup的dispatchTouchEvent返回true的时候三个action都能执行到,而返回false的话,只能执行到action_down,这个需要到view/Viewgroup的父类中dispatchTouchEvent找答案,该方法中会在action_down的时候调用dispatchTransformedTouchEvent方法,而该方法是通过子view的dispatchTouchEvent方法的返回值来决定父类的dispatchTransformedTouchEvent方法的返回值,而dispatchTransformedTouchEvent的返回值会决定mFirstTouchTarget是否为空,所以在action_down的过程中实际中通过子view的dispatchTouchEvent方法返回值来确定mFirstTouchTarget是否为空。这里贴出viewgroup中dispatchTransformedTouchEvent方法的删减代码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ------------------ //省略了cancel部分的代码 ------------------------ //如果child为空,直接调用自己的dispatchTouchEvent方法,此时自己就相当于一个view,touch事件走自己的 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); } transformedEvent.recycle(); return handled; } 复制代码
所以如果view/viewgroup的dispatchTouchEvent方法返回false,表示在action_down的时候,父类的dispatchTransformedTouchEvent方法返回false;如果返回true会调用addTouchTarget方法,给mFirstTouchTarget设置值:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } 复制代码
紧接着在在后面又会调用了:
这句只有在view/viewgroup的dispatchTouchEvent返回false的时候,才会走这里,所以后面的action_move和action_up都会走这里,而此时传入的child=null,从上面代码可以看到,直接调用了父类的dispatchTouchEvent方法。所以从这里不难看出在view/viewgroup的dispatchTouchEvent返回false的时候直接调用了父类的dispatchTouchEvent方法,因此只有action_down事件。
其实这道题考察大家对view的dispatchTouchEvent和view的onTouchEvent事件的处理流程,上面已经分析了想要view能执行到view的touch事件,那么必须要求view的dispatchTouchEvent返回true,而dispatchTouchEvent返回true要么是dispatchTouchEvent直接返回true或者view的onTouchEvent返回true。如果从效率上看,直接将dispatchTouchEvent返回true就ok,而不需要再去关心onTouchEvent方法。
关于拦截无非就是拦截或不拦截,而拦截的条件是返回true,不拦截是返回false或返回super.onInterceptTouchEvent,默认的super是返回false的,因此可以用super表示不拦截
viewgroup拦截实际是通过在dispatchTouchEvent方法中,设置intercepted变量,如果在拦截方法里面返回true,那么intercepted为true,如果为true则在action_down的时候mFirstTouchTarget=null,那么此时是直接调用dispatchTransformedTouchEvent传入的child=null,因此将事件交给了super.dispatchTouchEvent,此时把它当成一个view来处理了。
先贴出事例代码:
testView在testViewgroup里面,testViewgroup在action_move的时候拦截(onInterceptTouchEvent在move返回true),testView不进行分发(dispatchTouchEvent返回true) 咋们通过log来看结果:
这里执行到TestViewgroup#dispatchTouchEvent的action_move之后就执行了TestView#dispatchTouchEvent的action_cancel,然后后面执行TestViewgroup#dispatchTouchEvent和TestViewgroup#onTouchEvent的action_move和action_up。 从前面viewgroup的dispatchTouchEvent分析知道,如果viewgroup在action_down中发现有子view的dispatchTouchEvent返回true,则mFirstTouchTarget不为空,紧接着在action_move的时候进行了拦截,则intercepted=true,既然在move过程中确定了intercepted=true,mFirstTouchTarget不为空,则可以看viewgroup.dispatchTouchEvent部分代码:
TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; //alreadyDispatchedToNewTouchTarget=false if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { //由于move过程中intercepted=true,则cancelChild=true final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //看到了没这里就是触发child的dispatchTouchEvent的action_cancel if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { //由于next=null,因此mFirstTouchTarget=null,所以在action_move刚进来的时候mFirstTouchTarget=null了, //待会我们通过反射看下该变量 mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } 复制代码
上面也说明了在action_move进来的时候先是触发了testView#dispatchTouchEvent的action_cancel,紧接着mFirstTouchTarget=null了,由于mFirstTouchTarget是viewgroup类中私有的变量,我们可以通过反射调用该变量看下是否为空:
//反射代码,关于反射可以看我之前的文章java反射整理
接着在testViewgroup#dispatchTouchEvent中获取mFirstTouchTarget属性:
通过上面可以验证刚才move过程中mFirstTouchTarget为空的判断,日志如下:
看到了没,第一次move的时候mFirstTouchTarget还不是null,第二次move的时候就是null了,因此在后续的move和up过程中,只会此处:
看到了没,这里传进去的child=null,根据上面分析可知,当为null的时候,只会触发super.dispatchTouchEvent,所以到了第二次的move之后,只能看到TestViewgroup的action_move和action_up了。
所以针对上面面试官提的问题,大家知道怎么说了吧,还是针对该问题做个小结:
先是down事件会经过viewgroup的dispatchTouchEvent,再到viewgroup的onInterceptTouchEvent,最后到view的dispatchTouchEvent,此时mFirstTouchTarget不为空,紧接着到了move首先到viewgroup的dispatchTouchEvent,再到viewgroup的onInterceptTouchEvent,由于在move过程中拦截了,紧接着走view的dispatchTouchEvent的action_cancel,此时接着把mFirstTouchTarget至为null,因此后续的move和up事件只会走viewgroup的dispatchTouchEvent和onTouchEvent。 画出一张图来给大家看下:
好了,关于这个问题告一段落了,如果分析有问题,大家可以提出疑问。
其实这个问题在上面分析中已经分析过了,testview的onTouchEvent中消费,所以在action_down中mFirstTouchTarget不为空,因此在action_move和action_up中mFirstTouchTarget还是不为空,所以不管手指是否已经离开了testview,action_move和action_up还是会走testview的dispatchTouchEvent和onTouchEvent。
首先确定action_down过程中mFirstTouchTarget是否为空,如果不为空,所以不管手指是否已经不在testView上了,action_move和action_up还是会在testView的onTouchEvent上进行消费的。
这个问题就没涉及viewgroup到view的事件传递,onTouch指setOnTouchListener的回调方法,它是优先于onTouchEvent事件的,大家可以看下view的dispatchTouchEvent中有如下代码:
我想这个地方不用多说吧,如果onTouch方法返回true,是不会触发onTouchEvent事件的,所以在开篇第二个问题:如果想屏蔽掉view的点击事件,只想要view的拖拽事件,该怎么处理,其实这里完全可以重写setOnTouchListener的onTouch方法,并且onTouch里面返回true就会屏蔽掉onClickListener事件。
onClick事件是在onTouchEvent消费事件中的action_up触发的,onTouch是在dispatchTouchEvent中触发的,所以onTouch要先于onClick事件,我们也可以通过onTouch返回true来屏蔽掉onClick事件。
好了,关于这次我面试中遇到的事件分发主要是上面这几个问题,大家有什么其他的问题,可以在评论区互动。
其实android事件分发核心是在viewgroup的dispatchTouchEvent的action_down过程中找到mFirstTouchTarget是否为空,通过反序遍历子view的dispatchTouchEvent的方法,如果发现有一个子view的dispatchTouchEvent方法返回true,那么mFirstTouchTarget就不为空,否则为空。如果mFirstTouchTarget不为空,那么action_move和action_up才会往下传递,如果在action_move和action_up过程中有viewgroup拦截了事件,则此时先向子view的dispatchTouchEvent传递一个action_cancel,并且将mFirstTouchTarget至为null,所以此时action_move和action_up只会走viewgroup的dispatchTouchEvent和onTouchEvent;如果mFirstTouchTarget在action_down过程中就已经null的话,则从action_down一直向上层view传递,不会有后续的action_move和action_up了。