聊聊Android事件分发的那点事

做Android也有一段时间了,反反复复用到一些事件分发的情况,反反复复被困扰,以下是一些总结,希望对大家有所帮助。直接进入主题

网上关于事件分发的文章也不少,有很多方式来理解事件分发机制,比如Demo,Log打印,源码分析等等,但是楼主今天准备把这些结合一下,用另外一种更加简单易懂的方式来说说这个事,希望能达到目的。

首先我们先弄清楚几个事情:

1,事件的分发主要涉及的部分:Activity,ViewGroup,View

    View的子类有TextView,ImageView等等,ViewGroup的子类有LinearLayout,RelativeLayout等,当然ViewGroup也是View的一个子类,如果这个地方还有问题的,也先不用特别在意,不影响对于事件分发的理解。

2,事件分发机制主要涉及的几个方法:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent(),以及OnTouchListener.onTouch();

    先说最最最基类的View,它本身拥有dispatchTouchEvent()和onTouchEvent()两个方法,并且拥有OnTouchListener的接口。用来处理touch事件

    中间说说ViewGroup,它是继承自View,自然也拥有上述方法,并且它重写了dispatchTouchEvent(),所以它的子类的分发都会走它的dispatch方法而不是View的,并且ViewGroup增加了一个方法onInterceptTouchEvent(),用来打断touch事件,这个我们后面再聊。

    最后我们说一下Activity,Activity拥有自己的dispatchTouchEvent()和onTouchEvent()。

3,上面说的这些方法都有一个boolean的返回值,返回true的话说明事件被消费,返回false说明事件没有被消费,不知道这个的童鞋可以先默默记一下,在往下看。

 

好,这些东西弄清楚了,我们可以往下走了。下面我想大致把事件分发的机制用简单概括的方式给童鞋说一下,让大家先有个大致的理解,我尽量写的通俗易懂

    事件分发主要涉及DOWN,MOVE,UP事件,首先当手指按下,DOWN事件触发,最外层的Activity的dispatchTouchEvent()最先接到DOWN消息,返回false没有消费,传递给View层,比如Activity的最外层是一个LinearLayout,简称A,A的dispatchTouchEvent()接到DOWN事件,因为LinearLayout是一个ViewGroup类型,它可能包含许多子View,所以dispatchTouchEvent()方法中调用了onInterceptTouchEvent(),如果返回false就会把事件分发给子View,反之则不会,当然默认是返回false;那么假设A有一个TextView的子View,简称B,这时B就会接到DOWN事件,B属于一个目标View,因为它不会再有子View了,这时会调用B的onTouchEvent()方法消费DOWN,如果返回false,向上传递给A,A的onTouchEvent()消费,如果返回false,则返回给Activity的OnTouchEvent()。

         

简单的画一个图大家可以看一下,如果一个DOWN事件没有消费的话,就会按照这样的逻辑走,在往回传递事件时,一旦返回为true,事件就会被消费掉

如果我们将LinearLayout的onInterceptTouchEvent中的返回值改为true,也就是不像下分发DOWN事件。

相信看到这里,大家对于事件分发应该有了一些比较深的认识,那么我们继续从源码的角度来分析一下整个事件分发的过程

第一个,我们先看看Activity的源码

 1   /**
 2      * Called to process touch screen events.  You can override this to
 3      * intercept all touch screen events before they are dispatched to the
 4      * window.  Be sure to call this implementation for touch screen events
 5      * that should be handled normally.
 6      * 
 7      * @param ev The touch screen event.
 8      * 
 9      * @return boolean Return true if this event was consumed.
10      */
11     public boolean dispatchTouchEvent(MotionEvent ev) {
12         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
13             onUserInteraction();
14         }
15         if (getWindow().superDispatchTouchEvent(ev)) {
16             return true;
17         }
18         return onTouchEvent(ev);
19     }

很简单,如果返回true,说明这个事件被消费掉了,会直接调用Activity的onTouchEvent()方法,看15行代码,我们追到Window的superDispatchTouchEvent()方法中

1   /**
2      * Used by custom windows, such as Dialog, to pass the touch screen event
3      * further down the view hierarchy. Application developers should
4      * not need to implement or call this.
5      *
6      */
7     public abstract boolean superDispatchTouchEvent(MotionEvent event);

看注释,它会将touch event传递到view层,所以我们不去关心这个,只需要知道如果这个touch事件没有被消费的话,它会传递到view层,那下一个接收到touch事件的应该是最外层的LinearLayoutA了

  1 @Override
  2     public boolean dispatchTouchEvent(MotionEvent ev) {
  3         if (mInputEventConsistencyVerifier != null) {
  4             mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
  5         }
  6 
  7         boolean handled = false;
  8         if (onFilterTouchEventForSecurity(ev)) {
  9             final int action = ev.getAction();
 10             final int actionMasked = action & MotionEvent.ACTION_MASK;
 11 
 12             // Handle an initial down.
 13             if (actionMasked == MotionEvent.ACTION_DOWN) {
 14                 // Throw away all previous state when starting a new touch gesture.
 15                 // The framework may have dropped the up or cancel event for the previous gesture
 16                 // due to an app switch, ANR, or some other state change.
 17                 cancelAndClearTouchTargets(ev);
 18                 resetTouchState();
 19             }
 20 
 21             // Check for interception.
 22             final boolean intercepted;
 23             if (actionMasked == MotionEvent.ACTION_DOWN
 24                     || mFirstTouchTarget != null) {
 25                 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 26                 if (!disallowIntercept) {
 27                     intercepted = onInterceptTouchEvent(ev);
 28                     ev.setAction(action); // restore action in case it was changed
 29                 } else {
 30                     intercepted = false;
 31                 }
 32             } else {
 33                 // There are no touch targets and this action is not an initial down
 34                 // so this view group continues to intercept touches.
 35                 intercepted = true;
 36             }
 37 
 38             // Check for cancelation.
 39             final boolean canceled = resetCancelNextUpFlag(this)
 40                     || actionMasked == MotionEvent.ACTION_CANCEL;
 41 
 42             // Update list of touch targets for pointer down, if needed.
 43             final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
 44             TouchTarget newTouchTarget = null;
 45             boolean alreadyDispatchedToNewTouchTarget = false;
 46             if (!canceled && !intercepted) {
 47                 if (actionMasked == MotionEvent.ACTION_DOWN
 48                         || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
 49                         || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 50                     final int actionIndex = ev.getActionIndex(); // always 0 for down
 51                     final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
 52                             : TouchTarget.ALL_POINTER_IDS;
 53 
 54                     // Clean up earlier touch targets for this pointer id in case they
 55                     // have become out of sync.
 56                     removePointersFromTouchTargets(idBitsToAssign);
 57 
 58                     final int childrenCount = mChildrenCount;
 59                     if (newTouchTarget == null && childrenCount != 0) {
 60                         final float x = ev.getX(actionIndex);
 61                         final float y = ev.getY(actionIndex);
 62                         // Find a child that can receive the event.
 63                         // Scan children from front to back.
 64                         final View[] children = mChildren;
 65 
 66                         final boolean customOrder = isChildrenDrawingOrderEnabled();
 67                         for (int i = childrenCount - 1; i >= 0; i--) {
 68                             final int childIndex = customOrder ?
 69                                     getChildDrawingOrder(childrenCount, i) : i;
 70                             final View child = children[childIndex];
 71                             if (!canViewReceivePointerEvents(child)
 72                                     || !isTransformedTouchPointInView(x, y, child, null)) {
 73                                 continue;
 74                             }
 75 
 76                             newTouchTarget = getTouchTarget(child);
 77                             if (newTouchTarget != null) {
 78                                 // Child is already receiving touch within its bounds.
 79                                 // Give it the new pointer in addition to the ones it is handling.
 80                                 newTouchTarget.pointerIdBits |= idBitsToAssign;
 81                                 break;
 82                             }
 83 
 84                             resetCancelNextUpFlag(child);
 85                             if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
 86                                 // Child wants to receive touch within its bounds.
 87                                 mLastTouchDownTime = ev.getDownTime();
 88                                 mLastTouchDownIndex = childIndex;
 89                                 mLastTouchDownX = ev.getX();
 90                                 mLastTouchDownY = ev.getY();
 91                                 newTouchTarget = addTouchTarget(child, idBitsToAssign);
 92                                 alreadyDispatchedToNewTouchTarget = true;
 93                                 break;
 94                             }
 95                         }
 96                     }
 97 
 98                     if (newTouchTarget == null && mFirstTouchTarget != null) {
 99                         // Did not find a child to receive the event.
100                         // Assign the pointer to the least recently added target.
101                         newTouchTarget = mFirstTouchTarget;
102                         while (newTouchTarget.next != null) {
103                             newTouchTarget = newTouchTarget.next;
104                         }
105                         newTouchTarget.pointerIdBits |= idBitsToAssign;
106                     }
107                 }
108             }
109 
110             // Dispatch to touch targets.
111             if (mFirstTouchTarget == null) {
112                 // No touch targets so treat this as an ordinary view.
113                 handled = dispatchTransformedTouchEvent(ev, canceled, null,
114                         TouchTarget.ALL_POINTER_IDS);
115             } else {
116                 // Dispatch to touch targets, excluding the new touch target if we already
117                 // dispatched to it.  Cancel touch targets if necessary.
118                 TouchTarget predecessor = null;
119                 TouchTarget target = mFirstTouchTarget;
120                 while (target != null) {
121                     final TouchTarget next = target.next;
122                     if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
123                         handled = true;
124                     } else {
125                         final boolean cancelChild = resetCancelNextUpFlag(target.child)
126                                 || intercepted;
127                         if (dispatchTransformedTouchEvent(ev, cancelChild,
128                                 target.child, target.pointerIdBits)) {
129                             handled = true;
130                         }
131                         if (cancelChild) {
132                             if (predecessor == null) {
133                                 mFirstTouchTarget = next;
134                             } else {
135                                 predecessor.next = next;
136                             }
137                             target.recycle();
138                             target = next;
139                             continue;
140                         }
141                     }
142                     predecessor = target;
143                     target = next;
144                 }
145             }
146 
147             // Update list of touch targets for pointer up or cancel, if needed.
148             if (canceled
149                     || actionMasked == MotionEvent.ACTION_UP
150                     || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
151                 resetTouchState();
152             } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
153                 final int actionIndex = ev.getActionIndex();
154                 final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
155                 removePointersFromTouchTargets(idBitsToRemove);
156             }
157         }
158 
159         if (!handled && mInputEventConsistencyVerifier != null) {
160             mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
161         }
162         return handled;
163     }

这个代码比较多,我们抽重点讲,首先看22行代码,可以看到intercepted参数,在看一下46行,intercepted的参数决定能不能讲touch事件分发到下一层View,而决定这个参数的重要地方是25 ~ 31行,25行的disallowIntercept参数可参考requestDisallowInterceptTouchEvent()方法,这里不细讲,就是可以让子View设置是否不让父View跳过onInterceptTouchEvent()方法。注:requestDisallowInterceptTouchEvent()方法如果灵活运用的话会很有用。如果我们什么都没设置的话,自然会进入到ViewGroup独有的onInterceptTouchEvent()方法中,我们看一下源码

 

   public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

 

很简单,默认是返回false的,也就是默认ViewGroup是不会拦截touch事件。我们可以实现这个函数根据自己的需要去返回true或者false。所以如果返回true的话,代码执行中会跳过46~108行,事件就不会分发到子View中。其他的代码对于我们今天讲的事情关系不大,有兴趣的童鞋可以继续研究,我们继续往下走,所以如果我们没有操作的话,这时Touch事件会进入到下一层View的dispatchTouchEvent()方法中,也就是TextViewB中,我们来看代码

 1    public boolean dispatchTouchEvent(MotionEvent event) {
 2         if (mInputEventConsistencyVerifier != null) {
 3             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
 4         }
 5 
 6         if (onFilterTouchEventForSecurity(event)) {
 7             //noinspection SimplifiableIfStatement
 8             ListenerInfo li = mListenerInfo;
 9             if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
10                     && li.mOnTouchListener.onTouch(this, event)) {
11                 return true;
12             }
13 
14             if (onTouchEvent(event)) {
15                 return true;
16             }
17         }
18 
19         if (mInputEventConsistencyVerifier != null) {
20             mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
21         }
22         return false;
23     }

先看8到12行的代码,if 语句里面的判断注意最后一个,li.mOnTouchListener.onTouch(this, event);这个li.mOnTouchListener 其实就是我们设置的onTouchListener。也就是说我们平时实现的OnTouchListener都会在这里起作用,而且如果我们实现的方法中返回true的话,就决定了这个事件会被消费,14行的代码就不会被执行,而View本身的OnTouchEvent()方法是不会执行的。当然,默认是返回发false的,所以大家可以灵活运用,如果只是想拿到Touch事件,做一些事情,那就老老实实的返回false吧。好继续走,到14行,开始执行onTouchEvent()方法,因为View只是一个父类,有很多子类,有的子类会重写OnTouchEvent来完成自己的特性,比如基于TextView为父类的一系列View,还有ListView,GridView等等。ImageView就没有重写OnTouchEvent。如果有童鞋有兴趣可以具体研究。我们这里主要看一下View本身的onTouchEvent()方法,这里看的很清楚,如果14行的onTouchEvent()返回是true的话,这个事件就会被消费掉了。

  1 /**
  2      * Implement this method to handle touch screen motion events.
  3      *
  4      * @param event The motion event.
  5      * @return True if the event was handled, false otherwise.
  6      */
  7     public boolean onTouchEvent(MotionEvent event) {
  8         final int viewFlags = mViewFlags;
  9 
 10         if ((viewFlags & ENABLED_MASK) == DISABLED) {
 11             if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
 12                 setPressed(false);
 13             }
 14             // A disabled view that is clickable still consumes the touch
 15             // events, it just doesn't respond to them.
 16             return (((viewFlags & CLICKABLE) == CLICKABLE ||
 17                     (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
 18         }
 19 =============================================================================================================================
 20         if (mTouchDelegate != null) {
 21             if (mTouchDelegate.onTouchEvent(event)) {
 22                 return true;
 23             }
 24         }
 25 
 26         if (((viewFlags & CLICKABLE) == CLICKABLE ||
 27                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
 28             switch (event.getAction()) {
 29                 case MotionEvent.ACTION_UP:
 30                     boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
 31                     if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
 32                         // take focus if we don't have it already and we should in
 33                         // touch mode.
 34                         boolean focusTaken = false;
 35                         if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
 36                             focusTaken = requestFocus();
 37                         }
 38 
 39                         if (prepressed) {
 40                             // The button is being released before we actually
 41                             // showed it as pressed.  Make it show the pressed
 42                             // state now (before scheduling the click) to ensure
 43                             // the user sees it.
 44                             setPressed(true);
 45                        }
 46 
 47                         if (!mHasPerformedLongPress) {
 48                             // This is a tap, so remove the longpress check
 49                             removeLongPressCallback();
 50 
 51                             // Only perform take click actions if we were in the pressed state
 52                             if (!focusTaken) {
 53                                 // Use a Runnable and post this rather than calling
 54                                 // performClick directly. This lets other visual state
 55                                 // of the view update before click actions start.
 56                                 if (mPerformClick == null) {
 57                                     mPerformClick = new PerformClick();
 58                                 }
 59                                 if (!post(mPerformClick)) {
 60                                     performClick();
 61                                 }
 62                             }
 63                         }
 64 
 65                         if (mUnsetPressedState == null) {
 66                             mUnsetPressedState = new UnsetPressedState();
 67                         }
 68 
 69                         if (prepressed) {
 70                             postDelayed(mUnsetPressedState,
 71                                     ViewConfiguration.getPressedStateDuration());
 72                         } else if (!post(mUnsetPressedState)) {
 73                             // If the post failed, unpress right now
 74                             mUnsetPressedState.run();
 75                         }
 76                         removeTapCallback();
 77                     }
 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                         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
 98                     } else {
 99                         // Not inside a scrolling container, so show the feedback right away
100                         setPressed(true);
101                         checkForLongClick(0);
102                     }
103                     break;
104 
105                 case MotionEvent.ACTION_CANCEL:
106                     setPressed(false);
107                     removeTapCallback();
108                     removeLongPressCallback();
109                     break;
110 
111                 case MotionEvent.ACTION_MOVE:
112                     final int x = (int) event.getX();
113                     final int y = (int) event.getY();
114 
115                     // Be lenient about moving outside of buttons
116                     if (!pointInView(x, y, mTouchSlop)) {
117                         // Outside button
118                         removeTapCallback();
119                         if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
120                             // Remove any future long press/tap checks
121                             removeLongPressCallback();
122 
123                             setPressed(false);
124                         }
125                     }
126                     break;
127             }
128             return true;
129         }
130 
131         return false;
132     }

这个方法中我们以19行为分界线,注意看10~18行,如果这个View是disable,但是是clickable的状态的话依旧会消费掉touch事件,所以大家要用的时候要注意,下面我们注意看一下26 ~ 27行,这个if判断很重要,因为我们可以看到这个OnTouchEvent()事件最终返回true或者false是由这个 if 决定的。那我们看一下 if 的条件((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE),很简单,如果这个View的viewFlag如果是CLICKABLE,或者LONG_CLICKABLE的话,这个事件注定要返回true,也就是会被消费掉。那么viewFlag是怎么决定的呢,View中有一个setFlags()方法,而我们的通常见到的setVisibility,setFocusable,setClickable,等等很多方法都会调到这个方法,这里不细讲。而 if 里面的代码其实就很简单了,它根据用户的行为更改了现在的状态,比如现在是否press状态,是否focus状态等等,当然也产生了一些click事件,longClick事件等等。最终,onTouchEvent方法返回false的话,就会一层一层向上传递,就如上面的图所示了。

 

讲到这里,楼主基本该讲的都讲完了,再讲一些在看代码时候了解到的其他知识,希望能对大家有益。

1,我们的Touch事件一般分为三个DOWN,MOVE,UP三个事件,当我们的DOWN事件传递下去,没有View消费,又传回来的话,MOVE事件和UP事件是不会进行分发的,这一点在ViewGroup中的onInterceptTouchEvent()方法的注释中有写到,其实想想android这样设计也是正确的,DOWN事件都不消费,那这个View也不会有click,press等等事件了...

2,如果某一个ViewGroup的onInterceptTouchEvent方法返回了true(消费掉该事件),那么后面的所有事件都不会经过onInterceptTouchEvent()方法,除了ACTION_CANCEL事件。会直接到该ViewGroup的OnTouchEvent方法中。

3,最后在总结一下,Touch事件肯定是从Activity开始传递,然后按照布局的层次一层一层往下传递,最终会传递到一个目标View,也就是你点击的那个点所在的View,可能是一个ViewGroup,也可能是一个View,比如说一个Button或者一个TextView,然后根据他们各自的OnTouchEvent方法决定是否消费。如果不消费的话,又会一层一层传递上来,直到有View消费。

 

各位童鞋如果有其他见解或者问题,希望我们可以共同探讨。

 

 

 

 

 

 

posted on 2013-09-09 18:51  sumirro  阅读(435)  评论(0编辑  收藏  举报

导航