点击事件的分发过程其实是对MotionEvent事件分发过程,当一个MotionEvent产生以后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程。点击事件的分发由三个重要的方法共同完成:dispatchTouchEvent,onInterceptTOuchEvent,onTouchEvent。
View里,有两个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev); public boolean onTouchEvent(MotionEvent ev);
ViewGroup里,有三个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev); public boolean onInterceptTouchEvent(MotionEvent ev); public boolean onTouchEvent(MotionEvent ev);
在Activity里,有两个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev); public boolean onTouchEvent(MotionEvent ev);
android中默认情况下事件传递是由最终的view的接收到,传递过程是从父布局到子布局,也就是从Activity到ViewGroup到View的过程,默认情况,ViewGroup起到的是透传作用。
触摸事件是一连串ACTION_DOWN,ACTION_MOVE..MOVE…MOVE、最后ACTION_UP,触摸事件还有ACTION_CANCEL事件。事件都是从ACTION_DOWN开始的,Activity的dispatchTouchEvent()首先接收到ACTION_DOWN,执行super.dispatchTouchEvent(ev),事件向下分发。
dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。
1 不消费ACTION_DOWN,后续收不到ACTION_MOVE、ACTION_UP
View中没有拦截器,只能调用onTouchEvent选择消费或者不消费该事件。上图中View的onTouchEvent方法返回false,一直将false返回最顶层的ViewGrouop,该事件后续的ACTION_MOVE,ACTION_UP都不会再调用。
注: 如果View是可点击的(Button),onTouchEvent默认返回True,ImageView 则默认返回false。
2 消费ACTION_DOWN
后续ACTION_MOVE 和 UP 在不被拦截的情况下都会找到这个View
3 ACTION_MOVE 和 UP 被上层拦截
4 全部被上层拦截(包括ACTION_DOWN,ACTION_MOVE,ACTION_UP)
android中的Touch事件都是从ACTION_DOWN开始的:
单手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP
多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOVE--ACTION_POINTER_UP---ACTION_UP.
参考:http://www.eoeandroid.com/thread-319301-1-1.html?_dsign=495a5374
5 源码解析
1/ dispatchTouchEvent 用来进行事件分发,如果事件能够传递到当前的View,那么此方法一定会被调用,返回结果受当前View的OnTouchEvent和下级View的,onInterceptTOuchEvent方法影响,表示是否消耗当前事件。
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event)
2/ onInterceptTouchEvent是ViewGroup提供的方法,默认返回false,返回true表示拦截。
* @param ev The motion event being dispatched down the hierarchy. * @return Return true to steal motion events from the children and have * them dispatched to this ViewGroup through onTouchEvent(). * The current target will receive anACTION_CANCEL
event, and no further * messages will be delivered here. */ public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
3/ onTouchEvent 在 dispatchTouchEvent中被调用,用来处理点击事件,返回结果表示是否消耗此事件。如果不消耗,在同一事件序列中,当前View无法再次接受到事件。
6 事件分发与onClick/onTouch 的关系
为同一个button同时设置click和touch的Listener
button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "onClick execute"); } }); button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "onTouch execute, action " + event.getAction());return false;
} });
当点击这个Button时的log如下:
可以看到,onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。
问题1 : 当把onTouch的方法返回值改成true时,则onClick不再执行
原因: 当事件分发到该Button时,会调用它的onDispatchTouchEvent方法,该方法源码中大致有一个这样的判断:如果onTouch方法返回true时,表示onTouch消耗了改事件直接返回true,onTouchEvent方法则不再被调用(当ACTION_UP事件传递进来时,onClick在该方法中被调用,所以onClick也不会被调用)
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
注: 不同版本的可能上述代码实现不相同,但是思想一致。
问题2:onTouch返回false时,在ACTION_DOWN事件中onDispatchTouchEvent应该返回false,后续的ACTION_UP应该不会被调用,为什么还有log。
原因: 分析onTouchEvent源码,如果view是可点击的,则默认返回true消耗该事件。如果view是不可点击的则返回false,例如ImageView。所以对于BUTTON第一次ACTION_DOWN的onDispatchTouchEvent返回的是true,而对于ImageView返回值为false,它的后续ACTION_MOVE/ACTION_UP不会再被调用。
1 public boolean onTouchEvent(MotionEvent event) { 2 final float x = event.getX(); 3 final float y = event.getY(); 4 final int viewFlags = mViewFlags; 5 final int action = event.getAction(); 6 7 if ((viewFlags & ENABLED_MASK) == DISABLED) { 8 if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { 9 setPressed(false); 10 } 11 // A disabled view that is clickable still consumes the touch 12 // events, it just doesn‘t respond to them. 13 return (((viewFlags & CLICKABLE) == CLICKABLE 14 || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) 15 || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); 16 } 17 if (mTouchDelegate != null) { 18 if (mTouchDelegate.onTouchEvent(event)) { 19 return true; 20 } 21 } 22 23 if (((viewFlags & CLICKABLE) == CLICKABLE || 24 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || 25 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { 26 switch (action) { 27 case MotionEvent.ACTION_UP: 28 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; 29 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { 30 // take focus if we don‘t have it already and we should in 31 // touch mode. 32 boolean focusTaken = false; 33 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 34 focusTaken = requestFocus(); 35 } 36 37 if (prepressed) { 38 // The button is being released before we actually 39 // showed it as pressed. Make it show the pressed 40 // state now (before scheduling the click) to ensure 41 // the user sees it. 42 setPressed(true, x, y); 43 } 44 45 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { 46 // This is a tap, so remove the longpress check 47 removeLongPressCallback(); 48 49 // Only perform take click actions if we were in the pressed state 50 if (!focusTaken) { 51 // Use a Runnable and post this rather than calling 52 // performClick directly. This lets other visual state 53 // of the view update before click actions start. 54 if (mPerformClick == null) { 55 mPerformClick = new PerformClick(); 56 } 57 if (!post(mPerformClick)) { 58 performClick(); 59 } 60 } 61 } 62 63 if (mUnsetPressedState == null) { 64 mUnsetPressedState = new UnsetPressedState(); 65 } 66 67 if (prepressed) { 68 postDelayed(mUnsetPressedState, 69 ViewConfiguration.getPressedStateDuration()); 70 } else if (!post(mUnsetPressedState)) { 71 // If the post failed, unpress right now 72 mUnsetPressedState.run(); 73 } 74 75 removeTapCallback(); 76 } 77 mIgnoreNextUpEvent = false; 78 break; 79 80 case MotionEvent.ACTION_DOWN: 81 mHasPerformedLongPress = false; 82 83 if (performButtonActionOnTouchDown(event)) { 84 break; 85 } 86 87 // Walk up the hierarchy to determine if we‘re inside a scrolling container. 88 boolean isInScrollingContainer = isInScrollingContainer(); 89 90 // For views inside a scrolling container, delay the pressed feedback for 91 // a short period in case this is a scroll. 92 if (isInScrollingContainer) { 93 mPrivateFlags |= PFLAG_PREPRESSED; 94 if (mPendingCheckForTap == null) { 95 mPendingCheckForTap = new CheckForTap(); 96 } 97 mPendingCheckForTap.x = event.getX(); 98 mPendingCheckForTap.y = event.getY(); 99 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 100 } else { 101 // Not inside a scrolling container, so show the feedback right away 102 setPressed(true, x, y); 103 checkForLongClick(0, x, y); 104 } 105 break; 106 107 case MotionEvent.ACTION_CANCEL: 108 setPressed(false); 109 removeTapCallback(); 110 removeLongPressCallback(); 111 mInContextButtonPress = false; 112 mHasPerformedLongPress = false; 113 mIgnoreNextUpEvent = false; 114 break; 115 116 case MotionEvent.ACTION_MOVE: 117 drawableHotspotChanged(x, y); 118 119 // Be lenient about moving outside of buttons 120 if (!pointInView(x, y, mTouchSlop)) { 121 // Outside button 122 removeTapCallback(); 123 if ((mPrivateFlags & PFLAG_PRESSED) != 0) { 124 // Remove any future long press/tap checks 125 removeLongPressCallback(); 126 127 setPressed(false); 128 } 129 } 130 break; 131 } 132 133 return true; 134 } 135 136 return false; 137 }
参考:http://blog.csdn.net/guolin_blog/article/details/9097463
7 ViewGroup的事件拦截
一般情况下,如果Layout定义了onTouch事件,同时在Layout中添加了两个按钮。当点击按钮时,onTouch事件不会被调用(ViewGroup中源码可知,调用子View中Button的dispatchTouchEvent返回true,后面的onTouch事件不再被调用)。 但是点击除按钮之外的区域则onTouch事件会被调用。
当重写Layout中的onIntercreptTouchEvent函数,返回true时,则事件不会被传递到子View,直接调用Viewgroup的onTouchEvent处理事件,不管点击哪里,它的onTouch都会被执行。
http://blog.csdn.net/guolin_blog/article/details/9153747
总结:
1、 一个事件序列,ACTIVON_DOWN,ACTION_MOVE,ACTION_UP。 当DOWN事件时顶层ViewGroup的dispatchTouchEvent返回false时,后续事件都不再执行。
2、可点击View的onTouchEvent默认返回true,其它返回false
3、ViewGroup中dispatchTouchEvent和View中的该函数实现不同,View中该函数的onTouch都会执行,如果返回false,再执行onTouchEvnet。 ViewGroup中,如果子View消耗了这次事件则它的onTouch事件不会被执行。