转载请声明出处
问题
在自定义控件和设置事件Listener的时候,很多人想当然,完全根据自己的意愿处理返回值,比如down事件我不想处理,我要交给view自己处理所以return false或者return super.onTouchEvent(event);;我要自己处理move事件,不想交给view处理所以return true。这些想法给自己也给别人带来了很多bug。很多android开发者会遇到这样的问题:
为什么我在自定义view的onTouchEvent中处理了touch事件后我的onClickListener,onLongClickListener没有执行了?
为什么onTouchEvent中down的时候return false就收不到后面的事件了?而在move的时候返回return false为什么还可以收到up事件?
为什么我自定义的view会误触发LongClick事件和Click事件?
为什么我给view设置了onTouchListener处理事件后会误触发LongClick事件和Click事件?
onTouchEvent,onTouchListener,onClickListener,onLongClickListener这些方法和接口在什么时候什么地方会被调用?
。。。。。。
这些典型的事件冲突问题,在看完这篇文章之后,你就知道这些问题的答案了。文章中贴的源码是android4.0的,看有中文注释的地方就行了。
ViewGroup事件分发
首先从ViewGroup开始讲。平时我们使用到的xxxLayout都是继承自ViewGroup,ViewGroup担任着事件分发者的角色,将TouchEvent分发给Layout中的子View,它自己也可以像View一样处理无View认领的事件,因为ViewGroup的父类是View。ViewGroup中分发事件的方法是dispatchTouchEvent。当一个TouchEvent产生的时候,当前Activity会将该event交给位于最外层的xxxLayout,xxxLayout中的dispatchTouchEvent得到执行,在dispatchTouchEvent中将event交给包含该touch区域的子View。除了Activity中最外层的layout,所有View获得的事件序列的开始都是down事件。要了解事件分发的过程,就需要看ViewGroup的dispatchTouchEvent源码了,下面的源码讲解是按顺序的,为了方便讲解,代码有省略,如果想要看完可以自己去找来看。 - ......
-
- if (actionMasked == MotionEvent.ACTION_DOWN) {
-
-
-
- cancelAndClearTouchTargets(ev);
- resetTouchState();
- }
可以看到,down事件到来的时候会重置所有状态并清空Target list,这个list是用来存放处理了event的子view的target的,暂时不用理会。继续,接着看下面的,注意两个变量
newTouchTarget和 alreadyDispatchedToNewTouchTarget - ......
- TouchTarget newTouchTarget = null;
- boolean alreadyDispatchedToNewTouchTarget = false;
- if (!canceled && !intercepted) {
- if (actionMasked == MotionEvent.ACTION_DOWN
- || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
- || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
- ......
-
- for (int i = childrenCount - 1; i >= 0; i--) {
- final View child = children[i];
- if (!canViewReceivePointerEvents(child)
- || !isTransformedTouchPointInView(x, y, child, null)) {
- continue;
- }
-
- newTouchTarget = getTouchTarget(child);
- if (newTouchTarget != null) {
-
-
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- break;
- }
- ......
-
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
-
- mLastTouchDownTime = ev.getDownTime();
- mLastTouchDownIndex = i;
- mLastTouchDownX = ev.getX();
- mLastTouchDownY = ev.getY();
-
- newTouchTarget = addTouchTarget(child, idBitsToAssign);
-
- alreadyDispatchedToNewTouchTarget = true;
- break;
- }
- }
- }
- }
- ......
先不管事件拦截啥的,看这段代码,就是看这两个变量newTouchTarget和alreadyDispatchedToNewTouchTarget在过了这段代码后的值有没有改变,在down的时候可以进入这里面,所以如果子view的dispatchTouchEvent如果返回false的话该子view的target就不会被添加到target list中了,两个变量的值没有被改变。但在move和up的时候就进不去这里,所以在move和up事件时newTouchTarget是null的,alreadyDispatchedToNewTouchTarget是false。好的,接着往下 - ......
-
-
- if (mFirstTouchTarget == null) {
-
-
-
- handled = dispatchTransformedTouchEvent(ev, canceled, null,
- TouchTarget.ALL_POINTER_IDS);
- } else {
-
-
-
- TouchTarget predecessor = null;
- TouchTarget target = mFirstTouchTarget;
- while (target != null) {
-
- final TouchTarget next = target.next;
-
- if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
-
- handled = true;
- } else {
-
- final boolean cancelChild = resetCancelNextUpFlag(target.child)
- || intercepted;
-
- if (dispatchTransformedTouchEvent(ev, cancelChild,
- target.child, target.pointerIdBits)) {
- handled = true;
- }
- if (cancelChild) {
- if (predecessor == null) {
- mFirstTouchTarget = next;
- } else {
- predecessor.next = next;
- }
- target.recycle();
- target = next;
- continue;
- }
- }
- predecessor = target;
- target = next;
- }
- }
- ......
注释已经写的很明白了,在注释中已经回答了一开始的几个问题了,处理down事件和处理down后面的事件有很大的区别,只要记住事件序列是以down开始的就可以了。 事件分发总结:
如果down事件没有被子view处理或者子view处理down事件后返回false,那么ViewGroup自己处理,也就是用父类View的代码处理,若父类给自己返回false的话,那么Activity不会把后面的move和up事件分发给ViewGroup了。 如果某一子view处理down事件并返回true,那么将该子view记录下来。后面move和up事件到来时直接将事件交给处理了down事件的该子view。 OK,既然事件已经分发至view中了。那我们就开始进入view中的事件处理讲解吧~ View事件处理
前面layout把事件分发到这里了,就算没有字view处理,layout也会交给父类也就是View处理,依然是这里。在View事件处理中,将会讲到View的dispatchTouchEvent,onTouchListener,onClickListener,onLongClickListener这些个方法和监听器的执行和回调时间点。前面看到,ViewGroup把event分给View的dispatchTouchEvent,现在,就从这里开始讲。 - public boolean dispatchTouchEvent(MotionEvent event) {
- ......
-
- if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
- mOnTouchListener.onTouch(this, event)) {
- return true;
- }
-
- if (onTouchEvent(event)) {
- return true;
- }
- ......
- return false;
- }
我把其他暂时不用考虑的省略了,突出重点方便理解。看到View的dispatchTouchEvent,我们就可以开始回答前面的一些问题了: 假如我给View设置了onTouchListener并且在onTouch处理后返回true,那么onTouchEvent就不会被执行了,也就是说onClickListener和onLongClickListener都不会被回调了。 假如我在onTouchListener的onTouch中处理down的时候返回false,move和up返回值先不用管,这时候如果move事件持续事件长的话会触发长按事件,长按事件不触发就会触发点击事件。 具体为什么,还要继续往下看,下面进入View的重头戏onTouchEvent中看看: - public boolean onTouchEvent(MotionEvent event) {
- ......
-
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_UP:
- ......
- if (!mHasPerformedLongPress) {
-
-
- removeLongPressCallback();
- ......
-
-
-
-
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
-
- if (!post(mPerformClick)) {
- performClick();
- }
- ......
- }
- ......
- break;
-
- case MotionEvent.ACTION_DOWN:
-
- mHasPerformedLongPress = false;
-
- if (performButtonActionOnTouchDown(event)) {
- break;
- }
-
-
- boolean isInScrollingContainer = isInScrollingContainer();
-
-
-
-
- if (isInScrollingContainer) {
- mPrivateFlags |= PREPRESSED;
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- } else {
-
- mPrivateFlags |= PRESSED;
- refreshDrawableState();
-
- checkForLongClick(0);
- }
- break;
-
- case MotionEvent.ACTION_CANCEL:
- mPrivateFlags &= ~PRESSED;
- refreshDrawableState();
- removeTapCallback();
- break;
-
- case MotionEvent.ACTION_MOVE:
- final int x = (int) event.getX();
- final int y = (int) event.getY();
-
-
- if (!pointInView(x, y, mTouchSlop)) {
-
- removeTapCallback();
- if ((mPrivateFlags & PRESSED) != 0) {
-
- removeLongPressCallback();
-
-
- mPrivateFlags &= ~PRESSED;
- refreshDrawableState();
- }
- }
- break;
- }
- return true;
- }
-
- return false;
- }
从源码可以看到,down事件到来的时候,View就开始为LongClick计时了,所以说,在开始计时后如果没有接收到后续事件的话,长按事件就会触发,这时候mHasPerformedLongPress被赋值为true,click事件就不会被执行了,如果在计时结束之前,传进来up事件的话, mHasPerformedLongPress还是false,长按回调消息被移除,执行click回调。到这里,已经可以回答前面的事件冲突问题了: 假如我自定义了View,复写了onTouchEvent,返回值是true或者false,并没有调用父类的代码,这时候设置了Listener当然不会被调用了,除非自己在这里面手动执行performLongClick()或performClick()。 假如我自定义了View,复写了onTouchEvent,在down处理完后又想将事件交给父类处理所以return super.onTouchEvent;接下来的move事件我不想给父类处理了,直接return true或false,这时候过一会儿就会触发长按事件;假如我在长按事件触发前处理完了up事件return super.onTouchEvent,这时候就会触发click事件。 这是在没有设置onTouchListener情况下的误触发,在设置了onTouchListener时的误触发这两个事件道理是一样的。要解决这些问题,可以参照view的处理方式,在覆盖父类方法的情况下手动执行回调。有时候使用别人写的控件的时候别人可能没有处理这些Listener,这时候怎么设置Listener都不会被回调的,需要自己修改。有时候自己并不想触发长按事件,那就想办法把回调消息删掉,可以用通过反射删除,后面讲仿QQ下拉刷新的时候会用到。 如果想要更清楚的了解android的事件分发机制,去仔细看源码吧。。。