首页 > 代码库 > Android事件分发机制的学习
Android事件分发机制的学习
最近被Android事件分发机制折磨的很烦躁,网上各种博客资料看完觉得还是得自己写一篇,一方面加深理解,另一方面希望能帮助到也同样在学习相关知识的童鞋们。
话不多说,直接开整。
当用户的手指点击到屏幕,便是整个事件的开始。
首先获取到该事件的是view层的控制者Activity,具体怎么获得我们不得而知,在此也不追究,而继续我们的主题。Activity获得事件后便执行它自身的方法:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
在这个方法中,首先判断是否是down事件,是的话就实现一个回调方法。当然这不是重点,重点是 getWindow().superDispatchTouchEvent(ev) 这个方法的返回值,如果返回true的话,那么该activity的 dispatchTouchEvent 方法直接返回true,不再执行下面的代码。若是这个方法返回false,那么将执行该activity的onTouchEvent 方法。那么我们就要首先看下getWindow()是什么,它的 superDispatchTouchEvent 方法又是如何:
public Window getWindow() { return mWindow; }
再继续看mWindow:
mWindow是这么初始化的:mWindow = PolicyManager.makeNewWindow(this);public static Window makeNewWindow(Context context) { return sPolicy.makeNewWindow(context); }继续寻找IPolicy接口的实现类Policy中:public PhoneWindow makeNewWindow(Context context) { return new PhoneWindow(context);}再继续看PhoneWindow:@Override public boolean superDispatchTrackballEvent(MotionEvent event) { return mDecor.superDispatchTrackballEvent(event); }而mDecor是PhoneWindow的内部类DecorView的对象:public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event);}decorView是FrameLayout的子类:private final class DecorView extends FrameLayout implements RootViewSurfaceTakerFrameLayout中并没有dispatchTouchEvent方法,而FrameLayout是ViewGroup的子类,所以终于来到来第一个重点:viewGroup的dispatchTouchEvent 方法。
1 @Override 2 public boolean dispatchTouchEvent(MotionEvent ev) { 3 if (!onFilterTouchEventForSecurity(ev)) { 4 return false; 5 } 6 7 final int action = ev.getAction(); 8 final float xf = ev.getX(); 9 final float yf = ev.getY(); 10 final float scrolledXFloat = xf + mScrollX; 11 final float scrolledYFloat = yf + mScrollY; 12 final Rect frame = mTempRect; 13 14 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 15 16 if (action == MotionEvent.ACTION_DOWN) { 17 if (mMotionTarget != null) { 18 // this is weird, we got a pen down, but we thought it was 19 // already down! 20 // XXX: We should probably send an ACTION_UP to the current 21 // target. 22 mMotionTarget = null; 23 } 24 // If we‘re disallowing intercept or if we‘re allowing and we didn‘t 25 // intercept 26 if (disallowIntercept || !onInterceptTouchEvent(ev)) { 27 // reset this event‘s action (just to protect ourselves) 28 ev.setAction(MotionEvent.ACTION_DOWN); 29 // We know we want to dispatch the event down, find a child 30 // who can handle it, start with the front-most child. 31 final int scrolledXInt = (int) scrolledXFloat; 32 final int scrolledYInt = (int) scrolledYFloat; 33 final View[] children = mChildren; 34 final int count = mChildrenCount; 35 36 for (int i = count - 1; i >= 0; i--) { 37 final View child = children[i]; 38 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE 39 || child.getAnimation() != null) { 40 child.getHitRect(frame); 41 if (frame.contains(scrolledXInt, scrolledYInt)) { 42 // offset the event to the view‘s coordinate system 43 final float xc = scrolledXFloat - child.mLeft; 44 final float yc = scrolledYFloat - child.mTop; 45 ev.setLocation(xc, yc); 46 child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 47 if (child.dispatchTouchEvent(ev)) { 48 // Event handled, we have a target now. 49 mMotionTarget = child; 50 return true; 51 } 52 // The event didn‘t get handled, try the next view. 53 // Don‘t reset the event‘s location, it‘s not 54 // necessary here. 55 } 56 } 57 } 58 } 59 } 60 61 boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || 62 (action == MotionEvent.ACTION_CANCEL); 63 64 if (isUpOrCancel) { 65 // Note, we‘ve already copied the previous state to our local 66 // variable, so this takes effect on the next event 67 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; 68 } 69 70 // The event wasn‘t an ACTION_DOWN, dispatch it to our target if 71 // we have one. 72 final View target = mMotionTarget; 73 if (target == null) { 74 // We don‘t have a target, this means we‘re handling the 75 // event as a regular view. 76 ev.setLocation(xf, yf); 77 if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { 78 ev.setAction(MotionEvent.ACTION_CANCEL); 79 mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 80 } 81 return super.dispatchTouchEvent(ev); 82 } 83 84 // if have a target, see if we‘re allowed to and want to intercept its 85 // events 86 if (!disallowIntercept && onInterceptTouchEvent(ev)) { 87 final float xc = scrolledXFloat - (float) target.mLeft; 88 final float yc = scrolledYFloat - (float) target.mTop; 89 mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 90 ev.setAction(MotionEvent.ACTION_CANCEL); 91 ev.setLocation(xc, yc); 92 if (!target.dispatchTouchEvent(ev)) { 93 // target didn‘t handle ACTION_CANCEL. not much we can do 94 // but they should have. 95 } 96 // clear the target 97 mMotionTarget = null; 98 // Don‘t dispatch this event to our own view, because we already 99 // saw it when intercepting; we just want to give the following100 // event to the normal onTouchEvent().101 return true;102 }103 104 if (isUpOrCancel) {105 mMotionTarget = null;106 }107 108 // finally offset the event to the target‘s coordinate system and109 // dispatch the event.110 final float xc = scrolledXFloat - (float) target.mLeft;111 final float yc = scrolledYFloat - (float) target.mTop;112 ev.setLocation(xc, yc);113 114 if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {115 ev.setAction(MotionEvent.ACTION_CANCEL);116 target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;117 mMotionTarget = null;118 }119 120 return target.dispatchTouchEvent(ev);121 }
首先第14行 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 中判断该ViewGroup的是否允许拦截事件的标志位,一般情况下disallowIntecept的值都是为false的,因为在67行中会在up或cancel事件中将这个标志位重置。
进入down事件判断后会将 mMotionTarget 置为null,而mMotionTarget是下面要寻找的目标。进入 disallowIntercept || !onInterceptTouchEvent(ev) 判断中, disallowIntercept为false,onInterceptTouchEvent(ev)方法直接返回false,所以结果为true进入该判断。在这个判断的for循环中,会一直遍历子view (VISIBLE且没有执行动画的view),直到找到包含被用户点击坐标的子view。进入 child.dispatchTouchEvent(ev) 判断中,如果该方法返回true,就将mMotionTarget设置为该view,并且返回true。如果没有找到,那么mMotionTarget便为null。
找到mMotionTarget的情况下,最终activity的dispatchTouchEvent方法返回true,表示down事件被mMotionTarget消费了。
没有找到mMotionTarget的情况下,看72行,target也就为null,进入target==null的判断,在81行返回super.dipatchTouchEvent,而viewGroup的父类是View,所以继续看View的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent event) { if (!onFilterTouchEventForSecurity(event)) { return false; } if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
依旧忽略第一个判断,看第二个判断:先看mOnTouchListener是否为null,也就是是否给view设置了onTouchListener;再看这个view是否enable;最后看设置的OnTouchListener 的 onTouch 方法的返回值是否为true。如果三个条件都满足,该方法返回true,ViewGroup的dispatchTouchEvent返回true,activity的dispatchTouchEvent返回true,表示该viewGroup消费了此事件。如果三个条件有一个不满足,那么执行view的 onTouchEvent 方法。
1 public boolean onTouchEvent(MotionEvent event) { 2 final int viewFlags = mViewFlags; 3 4 if ((viewFlags & ENABLED_MASK) == DISABLED) { 5 // A disabled view that is clickable still consumes the touch 6 // events, it just doesn‘t respond to them. 7 return (((viewFlags & CLICKABLE) == CLICKABLE || 8 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); 9 } 10 11 if (mTouchDelegate != null) { 12 if (mTouchDelegate.onTouchEvent(event)) { 13 return true; 14 } 15 } 16 17 if (((viewFlags & CLICKABLE) == CLICKABLE || 18 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { 19 switch (event.getAction()) { 20 case MotionEvent.ACTION_UP: 21 boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; 22 if ((mPrivateFlags & PRESSED) != 0 || prepressed) { 23 // take focus if we don‘t have it already and we should in 24 // touch mode. 25 boolean focusTaken = false; 26 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 27 focusTaken = requestFocus(); 28 } 29 30 if (!mHasPerformedLongPress) { 31 // This is a tap, so remove the longpress check 32 removeLongPressCallback(); 33 34 // Only perform take click actions if we were in the pressed state 35 if (!focusTaken) { 36 // Use a Runnable and post this rather than calling 37 // performClick directly. This lets other visual state 38 // of the view update before click actions start. 39 if (mPerformClick == null) { 40 mPerformClick = new PerformClick(); 41 } 42 if (!post(mPerformClick)) { 43 performClick(); 44 } 45 } 46 } 47 48 if (mUnsetPressedState == null) { 49 mUnsetPressedState = new UnsetPressedState(); 50 } 51 52 if (prepressed) { 53 mPrivateFlags |= PRESSED; 54 refreshDrawableState(); 55 postDelayed(mUnsetPressedState, 56 ViewConfiguration.getPressedStateDuration()); 57 } else if (!post(mUnsetPressedState)) { 58 // If the post failed, unpress right now 59 mUnsetPressedState.run(); 60 } 61 removeTapCallback(); 62 } 63 break; 64 65 case MotionEvent.ACTION_DOWN: 66 if (mPendingCheckForTap == null) { 67 mPendingCheckForTap = new CheckForTap(); 68 } 69 mPrivateFlags |= PREPRESSED; 70 mHasPerformedLongPress = false; 71 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 72 break; 73 74 case MotionEvent.ACTION_CANCEL: 75 mPrivateFlags &= ~PRESSED; 76 refreshDrawableState(); 77 removeTapCallback(); 78 break; 79 80 case MotionEvent.ACTION_MOVE: 81 final int x = (int) event.getX(); 82 final int y = (int) event.getY(); 83 84 // Be lenient about moving outside of buttons 85 int slop = mTouchSlop; 86 if ((x < 0 - slop) || (x >= getWidth() + slop) || 87 (y < 0 - slop) || (y >= getHeight() + slop)) { 88 // Outside button 89 removeTapCallback(); 90 if ((mPrivateFlags & PRESSED) != 0) { 91 // Remove any future long press/tap checks 92 removeLongPressCallback(); 93 94 // Need to switch from pressed to not pressed 95 mPrivateFlags &= ~PRESSED; 96 refreshDrawableState(); 97 } 98 } 99 break;100 }101 return true;102 }103 104 return false;105 }
先看第4行判断,如果这个view是disable的话,返回这个view是否是clickable的或者longClickable的,意思就是这个view如果可点击或者可长点击,onTouchEvent直接返回true,表示该view消费了此事件。
再看第11行,如果这个view设置了代理,那么事件交给代理处理。这个功能很奇怪,大概就是点击一个地方,而另一个地方会有反馈信息,很不符合正常人的使用习惯,可能有一些特殊用途吧,不然用户还觉得屏幕坏了...
接着第17行,如果view是可点击或者可长点击的话进入判断。还是分析down事件。
if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); }
先确保mPendingCheckForTap 有个实例对象,mPendingCheckForTap是什么呢?
private CheckForTap mPendingCheckForTap = null;
private final class CheckForTap implements Runnable { public void run() { mPrivateFlags &= ~PREPRESSED; mPrivateFlags |= PRESSED; refreshDrawableState(); if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { postCheckForLongClick(ViewConfiguration.getTapTimeout()); } } }
从第69行,将记录状态值的mPrivateFlags设置为PREPRESSED(可以理解为即将点击的状态),将 mHasPerformedLongPress设置为false(可以理解为还没有执行长点击动作),再post一个延时任务(因为view最终继承的是handler,所以可以发送消息)。ViewConfiguration.getTapTimeout()返回的是一个常量TAB_TIMEOUT == 115,表示115ms后执行这个任务。如果执行了mPendingCheckForTap就是指执行了CheckForTap的 run 方法。先将mPrivateFlags设置为PRESSED,之后 refreshDrawableState 方法中进行了一些操作,在此不作分析。接着进入判断如果这个view是可长点击的,进入方法 postCheckForLongClick,参数依旧是TAB_TIMEOUT。
private void postCheckForLongClick(int delayOffset) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }
在此方法中先确保 mPendingCheckForLongPress 有实例对象,再来看下CheckForLongPress,也是实现Runnable的类
class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } }
忽略这个方法 mPendingCheckForLongPress.rememberWindowAttachCount(); 之后又是post 一个延时的消息,延时LONG_PRESS_TIMEOUT - TAB_TIMEOUT,也就是500 - 115,确保你从按下到执行此事件用的时间是500ms。那么具体就要看 CheckForLongPress 的run 方法了。run 方法中的第一个判断我们也不做深究,所以就看performLongClick 方法。
public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; if (mOnLongClickListener != null) { handled = mOnLongClickListener.onLongClick(View.this); } if (!handled) { handled = showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
依旧忽略一些。看第一个判断,如果设置了onLongClickListener,并且onLongClick 方法也返回true,那么handled 也就为true,此方法返回true。那么在CheckForLongPress中mHasPerformedLongPress也设置为true,表示已经执行了长点击事件。
down事件结束,其中因为长按事件是一个延迟执行的事件,所以在之后的事件中还是有可能被取消的,我们继续往下看后面的事件。
先看move。
依旧是actiivty到viewGroup,从ViewGroup的dispatchTouchEvent开始看。
如果在down事件中找到了target,并且传递过程中某个view 重写 onInterceptTouchEvent 方法使之返回值为true的话,直接进入代码中的86行的判断。将target的坐标转化为这个中断事件view的坐标,将事件改为cancel事件等操作之后,返回true,activity中也返回true,表示该view消费了move事件。
如果在down事件中找到了target,并且没有被中断,直到120行执行target.dispatchTouchEvent ,还是之前那些判断,看有没重写dispatchTouchEvent方法,没的话看有没添加监听,没的话执行onTouchEvent方法。在View的onTouchEvent方法中,从代码81行开始。先获取起始坐标,获取系统预设的一个值mTouchSlop(一直跟过去发现这个值预设的是16px,但获取出来的时候是根据当前机器有个缩放的过程的 mTouchSlop = (int) (density * TOUCH_SLOP + 0.5f);)。之后判断是否移动超过控件,超过的话就取消down事件中的事件回调。如果mPrivateFlags中设置了标记为PRESSED,那么移除长点击事件的回调,将PRESSED标志位移除,刷新界面,相当于给用户一个从被点击到点击的反馈。
如果在down事件中没找到target,那么进入第81行执行View的dispatchTouchEvent,之后过程基本同上,不再赘述。
再看up。
在viewGroup的dispatchTouchEvent中67行,将mGroupFlags的 FLAG_DISALLOW_INTERCEPT标志位取消,以便在下次事件中 disallowIntercept的值依然为false。之后的一些判断与move事件类似,不同点在View的onTouchEvent中。
先看这个 boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; 这个值如果第一个延时消息(115ms的那个)没执行,那么为true,执行了的话为false。之后的判断能进去,忽略 关于focus的判断和操作。下面判断 mHasPerformedLongPress的值,如果长点击还未执行,那么易移除掉长点击事件的延时任务,之后执行点击事件。
if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick();}private final class PerformClick implements Runnable { public void run() { performClick(); }}public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false;}
在performClick方法里,判断是否设置了OnClickListener。如果没设置直接返回false,设置了的话,先声音反馈,之后回调listener的onClick 方法,并且返回true。之后的内容就是将mPrivateFlags中PRESSED标记为职位1,然后 refreshDrawableState ,post 个延时消息设置pressed为false,最后移除第一个延时任务。
private final class UnsetPressedState implements Runnable { public void run() { setPressed(false); } }
View的onTouchEvent中的cancel方法就几句话,取消PRESSED标志位,刷新界面给反馈,移除延时任务。
mPrivateFlags &= ~PRESSED;refreshDrawableState();removeTapCallback();
总结:
整个事件分发过程对本人来说还是非常的复杂,其中很多细节没有深究,稍稍的总结一些重点:
down事件是唯一一个必须要发生的。
事件传递的过程中ViewGroup可以重写OnInterceptTouchEvent 方法来拦截事件,自己处理而不交给子view处理。
如果一个view是clickable或者longClickable(就算是disable)那么肯定会消费事件。
触发longClick事件只需要down事件即可,而click事件却还需要up事件。
由于整个事件分发的流程复杂,本人才疏学浅加上时间不足,先大致总结这些。当然这其中不乏错误或者一些不严谨的地方,希望各位大牛指正。
Android事件分发机制的学习