首页 > 代码库 > 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事件分发机制的学习