安卓中的事件分发机制主要涉及到两类控件,一类是容器类控件ViewGroup,如常用的布局控件,另一类是显示类控件,即该控件中不能用来容纳其它控件,它只能用来显示一些资源内容,如Button,ImageView等控件。暂且称前一类控件为ViewGroup类控件(尽管ViewGroup本身也是一个View),后者为View类控件。
安卓中的事件分发机制主要涉及到dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent ev)这三个方法,下面先简单的介绍一下各自的作用。
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发,如果事件能够传递给当前View则此方法一定会调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的返回值的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent函数的内部被调用,用来判断是否拦截某个事件,注意View类的控件不包含此方法,即一旦点击事件传递给View,那么View的onTouchEvent方法将会被调用,而ViewGroup默认是不拦截任何事件,即Android源码中ViewGroup的onInterceptTouchEvent(MotionEvent ev)方法默认返回false,这一点稍后再源码解析中可以看到。
public boolean onTouchEvent(MotionEvent ev)
在dispatchTouchEvent函数的内部被调用,用来处理点击事件,返回结果表示是否消耗当前事件
这三者之间的关系可以用如下结论来描述:
对于一个ViewGroup而言,点击事件产生后首先会传递给它,它的dispatchTouchEvent函数将会被调用,如果该ViewGroup的onInterceptTouchEvent返回true,则表示它要拦截当前事件,那么事件就会交给该ViewGroup处理,即它的 onTouchEvent方法会被调用,如果该ViewGroup的onInterceptTouchEvent返回false则表示它不拦截当前事件(默认返回false),这时事件就会传递给它的子控件,即子控件的dispatchTouchEvent函数将会被调用,整个事件分发过程重复上述过程,直至事件被处理。
当事件被传递到View控件时,则它一定会处理该事件,View控件不存在onInterceptTouchEvent方法,此时如果该View设置了OnTouchListener,那么OnTouchListener中的onTouch方法将会被调用,如果onTouch方法返回false,那么该View的onTouchEvent方法将会被调用,否则onTouchEvent方法不会被调用,在onTouchEvent方法中,如果该Vie设置了OnClickListener,那么它的onClick方法将会被调用。
安卓中的事件传递过程遵循如下顺序:Activity->Window->View,即当点击事件产生后,首先传递给Activity,Activity在传递给Window,最后Window传递给根View,根View接收到点击事件后,将会按照前面讲述的事件分发机制去分发事件。直至事件传递给非容器类View,如果一个非容器类的View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,以此类推,如果所有的View都不处理该事件,那么这个事件最终会交给Activity处理。
下面我们从源码的角度去验证上述结论与过程。
一Activity对点击事件的分发过程
点击事件用MotionEvent来表示,我们知道一个点击事件首先被Activity捕获到,随后Activity的dispatchTouchEvent将会被调用,我们来看一下Activity的dispatchTouchEvent的源码:
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }可以看到当我们点击手机屏幕时,即产生MotionEvent.ACTION_DOWN动作时,会调用onUserInteraction()方法,该方法是一个空的方法,即不产生任何效果,然后会执行getWindow().superDispatchTouchEvent(ev),而getWindow()方法将会得到一个Window对象,然后会调用Window的superDispatchTouchEvent(ev)方法,这也就验证了前面我们所说的Activity会将点击事件传递给Window,如果getWindow().superDispatchTouchEvent(ev)返回true则Activity的dispatchTouchEvent将会直接返回true,如果返回false则意味着点击事件在传递的过程中没人处理,即所有View的onTouchEvent方法都返回了false,那么Activity的onTouchEvent将会被调用。
至此Activity已经将事件传递给了Window,接下来我们来看一下,Window是如何将事件传递给ViewGroup的,Window是个抽象类,getWindow().superDispatchTouchEvent(ev)语句调用的事实上其子类的superDispatchTouchEvent(ev)方法,下面是其子类PhoneWindow的superDispatchTouchEvent(ev)方法源码:
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }可以看到在PhoneWindow的superDispatchTouchEvent方法中调用来mDecor的superDispatchTouchEvent方法,而mDecor本质上是一个DecorView,到这里也就验证了上面说的Window将会把点击事件传递给DecorView。那么我们接下来了解一下DecorView:
private final class DecorView extends FrameLayout implements RootViewSurfaceTakerDecorView被定义为PhoneWindow的一个内部类,它是整个Window的dingjiView(This is the top-level view of the window),可以看到DecorView继承自FrameLayout,即它本质上是一个FrameLayout,我们知道通常我们在Activity中获取某个控件的id使用findViewById但事实上Activity中的findViewById内部调用的是Window中的findViewById方法,Activity中的findViewById源码如下:
public View findViewById(int id) { return getWindow().findViewById(id); }
而Window中的findViewById调用的是DecorView中的findViewById,代码如下:
public View findViewById(int id) { return getDecorView().findViewById(id); }同样的我们在Activity中使用的setContentView最终调用的也是DecorView中的setContentView,也就是说我们通过setContentView设置的布局View是DecorView的一个子View,这也就验证了上面说的DecorView会将点击事件传递给根View,这个根View是我们通过setContentView设置的布局文件的根View,一般来说根View都属于ViewGroup类。
二根View对点击事件的分发过程
这个是安卓事件分发机制中最重要的一部分。我们首先来看一下ViewGroup的dispatchTouchEvent方法,这个方法处理的逻辑很多,所以代码比较复杂,如下:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 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(); } // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; 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) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; 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; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 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) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; 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; } } // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }代码很长,我们先观其大概,然后挑重点分析,首先我们来看一下如下的部分代码:
// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }从注释上可以看到这部分代码的功能是检查是否拦截事件(Check for interception),可以看到ViewGroup在两种情况下会判断是否拦截当前事件,即事件类型为MotionEvent.ACTION_DOWN(actionMasked == MotionEvent.ACTION_DOWN)或者mFirstTouchTarget != null,ACTION_DOWN这个很好理解,那么mFirstTouchTarget != null是什么意思呢?从后面的代码可以看到当事件被ViewGroup的子控件成功处理时,mFirstTouchTarget会被赋值为执行该子控件,即当ViewGroup不拦截事件而是将事件交给其子控件处理时mFirstTouchTarget != null,换而言之,如果事件被当前ViewGroup拦截则mFirstTouchTarget == null,那么当.ACTION_DOWN与.ACTION_UP事件产生时,则因为actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null不成立,那么该if语句将不会被执行,即ViewGroup的onInterceptTouchEvent(ev)将不会被调用,即当某个ViewGroup决定拦截一个事件后,那么这个事件序列都将由它来处理,而当其它事件序列产生时不再调用它的onInterceptTouchEvent(ev)去判断是否拦截余下事件序列。通俗的说就是,如果你的手指点击按下(ACTION_DOWN)事件被该ViewGroup拦截,那么其余的如ACTION_MOVE,ACTION_UP都将被它处理。
上面我们分析了ViewGroup拦截事件的条件,那么接下来我们看一下ViewGroup不拦截事件的条件,即将事件交给其子控件处理的过程,代码如下:
// Find a child that can receive the event. // Scan children from front to back. final View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; 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; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } }从注释上可以看到这段代码的作用是找一个可以处理事件的子控件(Find a child that can receive the event.),其过程是遍历ViewGroup中全部的子控件,然后判断该子控件是否可以接受到点击事件,这主要是通过两个条件来判断的:子控件是否在播放动画,点击事件的坐标是否在该子控件的范围内,第二个条件很好理解,在其范围内则表示用户点击的是该控件,那么点击事件自然被它接收。如果某个子控件满足上述两个条件,那么事件将会传递给它来处理。即调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),在该方法中的第三个参数即为上述遍历过程中找到的满足上述两个条件的子控件,而在该方法的内部事实上就是直接调用了子控件的dispatchTouchEvent方法,这样事件就交给了子控件去处理,代码如下:
/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; }可以看到,如果child不为空,则会调用子控件的dispatchTouchEvent(event)方法然后将其返回值赋给 handled最终返回。所以如果子元素的dispatchTouchEvent(event)方法返回true,那么其父控件的dispatchTransformedTouchEvent也将会返回true,那么包含if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))的if语句逻辑成立,将会执行该if语句,即如下代码将会被执行:
newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break;
即完成 newTouchTarget的赋值,然后跳出for循环,终止对子元素的遍历(break),子元素的dispatchTouchEvent(event)方法返回false,那么其父控件的dispatchTransformedTouchEvent也将会返回false,那么该if语句不成立,所以ViewGroup将会把事件交给下一个子控件处理(如果下一个子控件存在的话)。
如果遍历所有的子控件后事件都没被处理,那么ViewGroup将会自己处理点击事件,这一版包括两种情况:第一种ViewGroup不包含子控件,另一种正如上面所分析的子控件处理了点击事件,但是在子控件dispatchTouchEvent(event)方法返回false,一般是子元素在onTouchEvent中返回了false。此时ViewGroup会自己处理点击事件,代码片段如下:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);从注释上可以看到,这段代码表示此时无子控件响应点击事件( No touch targets ),此时点击事件会交给该ViewGroup自己处理,注意此时的dispatchTransformedTouchEvent函数的第三个参数传入的是null,根据我们前面的分析此时,如下语句将会被执行:
if (child == null) { handled = super.dispatchTouchEvent(event);}即此时点击事件传递给了View的dispatchTouchEvent方法,即点击事件开始交给View来处理。
三View对点击事件的分发过程
View对点击事件的处理过程相对而言就很简单,(此处说的View不包括容器类GroupView),因为View中不存在onInterceptTouchEvent方法,即如果点击事件传递给了View那么它一定会被该View处理。同样的我们首先来看一下View的dispatchTouchEvent方法。代码如下:
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }可以看到View对点击事件的处理过程首先会判断是否设置过OnTouchListener(li != null && li.mOnTouchListener != null),View是否处于可以点击状态(mViewFlags & ENABLED_MASK) == ENABLED),View的OnTouchListener中的onTouch方法是否返回true( li.mOnTouchListener.onTouch(this, event)),这三个条件如果同时成立,那么View的dispatchTouchEvent方法将直接返回true,即不会调用View的onTouchEvent(event),否则将会调用下面的if语句,即调用onTouchEvent(event)。我们来看一下View的onTouchEvent(event)方法,代码如下:
* @param event The motion event. * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true); } if (!mHasPerformedLongPress) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // 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(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_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. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); 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 removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
从第一个if判断语句可以看到,即使当前View处于不可点击状态((viewFlags & ENABLED_MASK) == DISABLED),该状态下的View照样会消耗点击事件(A disabled view that is clickable still consumes the touch events)。
我们重点看一下上述代码中的第三个if语句,这个是onTouchEvent对点击事件的具体处理,从上面的代码可以看到只要View的CLICKABLE与LONG_CLICKABLE中某一个为true,那么该View就会消耗点击事件。即onTouchEvent方法会返回true,而View的LONG_CLICKABLE属性默认为false,即通常View的onTouchEvent返回值很大程度上取决于CLICKABLE属性,而CLICKABLE属性与具体的View相关,具体来说如果该View为可点击的View则其CLICKABLE为true,不可点击的View其CLICKABLE属性为false,如Button是可点击的,TextView是不可点击的,通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性,另外,setOnClickListener会自动将View的CLICKABLE设置为true,setOnLongClickListener会自动将View的LONG_CLICKABLE设置为true。
另外可以看到当ACTION_UP事件发生时,会调用performClick方法,如果该View设置过OnClickListener,那么在performClick方法的内部会调用onClick方法。
好了以上就是本人理解的关于安卓中事件分发机制,看官如果觉得不错,请记得点个赞哦