转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38141719
问题
在自定义控件和设置事件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事件分发
...... // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. 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) { ...... //遍历子view 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) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } ...... //如果child的dispatchTouchEvent返回true,dispatchTransformedTouchEvent也会返回true,这时候条件就会成立,newTouchTarget和alreadyDispatchedToNewTouchTarget就会被修改。这里传进去child,方法里就会调用child.dispatchTouchEvent,如果传null就会调用super.dispatchTouchEvent if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = i; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //将处理了down事件的view的target添加到target list的头部,此时newTouchTarget和mFirstTouchTarget是相等的 newTouchTarget = addTouchTarget(child, idBitsToAssign); //标记事件已经分发给子view alreadyDispatchedToNewTouchTarget = true; break; } } } } ......
list中了,两个变量的值没有被改变。但在move和up的时候就进不去这里,所以在move和up事件时newTouchTarget是null的,alreadyDispatchedToNewTouchTarget是false。好的,接着往下
...... //mFirstTouchTarget是一个全局变量,指向target list的第一个元素。 // Dispatch to touch targets. if (mFirstTouchTarget == null) { //进入这里,说明前面down事件处理的子view返回false,导致没有target被添加到list中,也就是这个事件没有view认领。 // No touch targets so treat this as an ordinary view. //这里有个参数传了null,方法里面会判断这个参数,如果为null就调用super.dispatchTouchEvent,也就是自己来处理event。由于down后面的事件都没法修改mFirstTouchTarget,所以之后的事件都在这里执行了,该子view就没法接收到后面的事件了 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { //能进入这里,说明子view在处理down事件后返回了true。后面的move和up事件会直接进入这里 // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { //这里遍历这个target list,挨个分发事件,为了方便理解,可以暂时认为list里面此时就只包含一个target的,也就是当前处理事件的view的target final TouchTarget next = target.next; //如果是down事件的话,就会进入这个if里面,由于down事件在前面已经处理了,所以直接handled = true。因为如果是move和up的话alreadyDispatchedToNewTouchTarget是false,newTouchTarget是null。 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { //handled会被返回到当前Activity的dispatchTouchEvent中,具体在Activity中怎么使用可以查看其源码 handled = true; } else { //move和up事件会进入这里 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //看这里,由此可知在move的时候返回true或者false只会影响到layout返回给Activity的值,由于不是down事件所以不会影响up事件的获取。 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开始的就可以了。
事件分发总结:
View事件处理
public boolean dispatchTouchEvent(MotionEvent event) { ...... //看这里吧,如果设置了onTouchListener就在这里回调的 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } //再来看这里,调用了onTouchEvent,onClickListener和onLongClickListener就在onTouchEvent里面回调的 if (onTouchEvent(event)) { return true; } ...... return false; }
我把其他暂时不用考虑的省略了,突出重点方便理解。看到View的dispatchTouchEvent,我们就可以开始回答前面的一些问题了:
public boolean onTouchEvent(MotionEvent event) { ...... //从if条件可以看到,基本的View在onTouchEvent中只处理click事件和longclick事件 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: ...... if (!mHasPerformedLongPress) { //能进入这里说明up的时候长按事件还没有被执行 // This is a tap, so remove the longpress check,将长按回调从消息列表删除 removeLongPressCallback(); ...... // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. //接下来执行点击事件的回调 if (mPerformClick == null) { mPerformClick = new PerformClick(); } //用PerformClick异步执行,若不成功再调用performClick() if (!post(mPerformClick)) { performClick(); } ...... } ...... break; case MotionEvent.ACTION_DOWN: //down的时候开始设置标记,标记整个事件过程中长按事件是否被执行了 mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. //这里判断父控件是否是一个可滚动的控件,如果是,长按事件的回调会被延长,CheckForTap类是实现了Runnable接口内部类,在其run方法中会执行checkForLongClick方法,将长按的回调放入handle消息列表中,一段时间后执行 if (isInScrollingContainer) { mPrivateFlags |= PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away mPrivateFlags |= PRESSED; refreshDrawableState(); //将长按回调放入消息列表,传入0表示经过默认时间后执行长按事件回调 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(); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button,手指移出view区域时会移除消息列表中的所有回调,包括长按的回调 removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } return true; } //如果一个View不可点击也不可长按,这里返回false会导致dispatchTouchEvent返回false return false; }
从源码可以看到,down事件到来的时候,View就开始为LongClick计时了,所以说,在开始计时后如果没有接收到后续事件的话,长按事件就会触发,这时候mHasPerformedLongPress被赋值为true,click事件就不会被执行了,如果在计时结束之前,传进来up事件的话,mHasPerformedLongPress还是false,长按回调消息被移除,执行click回调。到这里,已经可以回答前面的事件冲突问题了: