1,Activity 对点击事件的分发过程
点击事件使用 MotionEvent 来表示,事件最先传递给 Activity,由Activity 的 dispatchTouchEvent 进行分发。如下代码
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
首先会判断 是否为 DOWN 事件,如果是 则会调用 onUserInteraction 方法,这个方法是个空的。需要去继承,如果该方法被调用,则表示 DOWN 事件产生了。需要在Activity 中实现。
接下来事件就会交给 Window 进行分发,如果返回 true,则这个事件到此就结束了。否则Activity的 onTouchEvent 就会被调用到。
那么Window 是如何将 事件给 ViewGroup 的?,查看源码可知,Window 是抽象类,superDispatchTouchEvent 也是抽象方法。因此必须找到他的实现类。
/** *用于自定义窗口,例如对话框,将trackball事件 *向下传递到视图层次结构中。应用程序开发人员应该 *不需要实现或调用它。 */ public abstract boolean superDispatchTouchEvent(MotionEvent event);
那么Window 的实现类是啥呢?
/** *顶层窗口外观和行为策略的抽象基类。这个类的一个 *实例应该用作添加到 *窗口管理器的顶级视图。它提供了标准的UI策略,如背景、标题 *区域、默认键处理等。 * *这个抽象类的唯一现有实现是 *android.view.PhoneWindow,您应该在需要 *窗口时实例化它。 */ public abstract class Window { }
由上面可知,Window 类的 实现类是 PhoneWindow。下面看一下 实现类的 superDispatchTouchEvent 方法。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
由代码可知, PhoneWindow 将 事件传给了 mDecor,那么这个 mDecor 是什么呢,如下:
// 这是窗口的顶层视图,包含窗口装饰 private DecorView mDecor; @Override public final View getDecorView() { if (mDecor == null || mForceDecorInstall) { installDecor(); } return mDecor; }
接着看一下 mDecor.superDispatchTouchEvent(event) 到底干了什么
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
可以看到他调用了 父类的分发的方法,那他的父类是谁呢?
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { }
由上可知,父类是 FrameLayout ,但是 FrameLayout 并没有 dispatchTouchEvent 方法,所以它调用的是 FrameLayout 父类的 dispatchTouchEvent 。如下:
public class FrameLayout extends ViewGroup { }
public abstract class ViewGroup extends View implements ViewParent, ViewManager { @Override public boolean dispatchTouchEvent(MotionEvent ev) { ....... } }
所以最后调用的是 ViewGroup 的 dispatchTouchEvent 方法。
经过这样的调用,事件就从 Activity 的 dispatchTouchEvent 传到了 ViewGroup 中。下面我们梳理一下流程
1,在Activity 中 调用 getWindow().superDispatchTouchEvent(ev) 方法,由于这个方法是 抽象方法,所以这里调用的是 PhoneWindow 的 superDispatchTouchEvent 方法
2,在 PhoneWindow 中 通过 getDecorView 方法获取到了 DecorView 的对象。然后在实现 实现的抽象方法中 调用了 mDecor.superDispatchTouchEvent(event)。
3,在 DecorView 类中 的 superDispatchTouchEvent 方法中 ,调用了 父类的 return super.dispatchTouchEvent(event) 方法,其实 DecorView 父类的 父类就是 ViewGroup ,而且其直接父类没有这个方法,所以这里直接调用的就是 ViewGroup 的 dispatchTouchEvent 方法。
4,到此 这个事件 就从 Activity 直接传到了 ViewGroup 中。
2,顶级ViewGroup 对点击事件分发的过程
点击事件到达 ViewGroup 以后,会调用ViewGroup 的 dispatchTouchEvent 方法 ,然后逻辑是这样的:如果ViewGroup 拦截了事件 即 onInterceptTouchEvent 返回了true,则事件由ViewGroup 处理,如果如果 ViewGroupo 的 mOnTouchListener 被设置,则onTouch 会被调用,否则 onTouchEvent 会被调用。也就是说 如果都有提供的话,onTouch 会屏蔽掉 onTouchEvent。在onTouchEvent 中,如果设置了 onClickListener,则onclick 会被调用。如果顶级 ViewGroup 不拦截事件,则事件会传递给他所在的点击事件链的子View。这时子View 的dispatchTouchEvent 会被调用。到此为止,事件已经从顶级View 传递给了 下一层 View ,接下来的传递过程 和 顶级View 是一致的。如此循环,完成整个事件的分发。
首先看一下 ViewGroup 中的事件分发。代码较长,我们分段来看
// 拦截检查 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; }
从第三行 可以看出事件类型如果为 DOWN 或者 mFirstTouchTarget !=null ,则ViewGroup 会判断是否要拦截事件。
DOWN 号理解,mFirstTouchTarget !=null 是什么呢?这个从后面的代码中可以看出来,当ViewGroup 的子元素处理成功时,mFirstTouchTarget 会被赋值并指向子元素。换种方式说,当ViewGroup 不拦截事件并将事件交由子元素处理时,mFirstTouchTarget !=null ,反过来,一旦事件被当前ViewGroup 拦截时, mFirstTouchTarget !=null 就不成立。
DOWN 事件 如果被拦截。那么当 MOVE 和 UP 事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null),这个条件为 false。将导致 ViewGroup 的 onInterceptTouchEvent 不会再被调用,并且同一序列中其他事件都会默认交给他 处理。由此也可以得出:如果 onInterceptTouchEvent 第一次返回了true。那么这个方法以后也不会进行调用。接下来这个同一个事件序列的事件都会被拦截。
当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是通过 requestDisallowInterceptTouchEvent 方法来设置的。一般用于子 View 中。FLAG_DISALLOW_INTERCEPT 一旦设置后,ViewGroup 将无法拦截 除了 ACTION_DOWN 以外的其他事件。为什么锁除了 ACTION_DOWN 以外的其他时间呢?这是因为在 ViewGroup 分发事件时,如果是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致 子View 中设置的标记位 无效,因此,当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法询问自己是否要拦截事件,这一点在源码也可以看出来。
在下面代码中,ViewGroup 会在 ACTION_DOWN 事件到来时做重置状态的操作,而在resetTouchState 方法中会对 FALG_DISALLOW_INTERCEPT 进行重置,因此子 View 调用 reqeustDisallowInterceptTouchEvent 方法并不能影响 ViewGroup 对 ACTION_DOWN 事件的处理 ,所以在 DOWN 事件开始的时候 会对标记位进行初始化。
// 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(); }
由此我们可以梳理一下:当 DOWN 事件来临的时候,上面第五行 通过标记位 获得的 boolear 为 false,则 就会进入下面的 if 判断。然后获取当前拦截的状态。如果没有拦截,则 事件会 传递到子View 中, 可以在子 View 中调用 reqeustDisallowInterceptTouchEvent 方法修改标记位,标记位修改后,在DOWN 以后的事件 都不会被拦截,因为修改完子View 修改完 标记位 后 上面 第5 行的 boolear 为true,则默认不拦截剩余事件,直到下一个DOWN来临。如果DOWN 事件被拦截,mFirstTouchTarget !=null 就不成立,意味着 这个事件系列里的余下所有事件 都会被默认拦截。由此可以得出,onInterceptTouchEvent 不是每次都会调用的。如果我们需要处理所有的点击事件,则需要选择 onInterceptTouchEvent 方法,只有这个方法才能保证每次都被调用。
那为什么 事件被拦截了 mFirstTouchTarget !=null 就不成立 呢?我们继续往下看。
接着看 ViewGroup 不拦截事件的时候,这段代码如下:
//事件没有被取消且没有被拦截 if (!canceled && !intercepted) { //....... 省略代码 // 遍历所有的子元素 final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //判断 1,View 可见并没有播放动画,2,点击事件坐标落在View范围内 //如果这两种有一个不满足 则退出本次循环 继续循环下一个View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); 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; } //重置子View 的标记 resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } }
上面首先遍历 ViewGroup 中所有的元素,然后判断子元素是否能够接收到点击事件。
刚哥说过:子元素是否能够接受点击事件主要由两点来衡量,子元素是否在播放动画 和 点击事件的坐标是否落在子元素的区域内。如果子元素满足这两个条件,那么事件就会传递给他来处理。
看上面第 41 行,dispatchTransformedTouchEvent 实际上调用的就是子元素 的 disPatchTouchEvent 方法。在这个方法的 内部有这么一段代码,如下:
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; }
如果这个孩子不为 null 就调用 孩子的 dispatchTouchEvent,否则就调用 父类的dispatchTouchEvent 。
最后 直接携带者 子元素 dispatchTouchEvent 的返回值 跳出这个方法
接着我们在看 41 行,如果 这个方法返回的是 true。则表示子 View 已经消费了这个事件。接着往下,第 57 行,如下
newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break;
我们看一下 addTouchTarget 方法
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
可以看到 上面这个行代码 完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历。如果 子元素的 dispatchTouchEvent 返回false,ViewGroup 就会把 事件分发给 下一个子元素(如果还有下一个子元素);
在 addTouchTarget 方法中,对 mFirstTouchTarget 进行了赋值。从这个结构可以看出 这应该是单链表的结构。这里就解决了上面的 一个疑问 为什么 事件被拦截了 mFirstTouchTarget !=null 就不成立 呢?,如果 事件没有被拦截,分发给了 子View,则 mFirstTouchTarget !=null 就是成立的。否则 肯定不会成立呀。
如果 遍历所有的子元素后 事件都没有被合适的处理,这里包含两种情况:1,ViewGroup 没有子元素 2,子元素处理了点击事件,但是在 dispatchTouchEvent 返回了false,这一般是因为 子元素在 onTouchEvent 中返回了 false。在这两种情况下,ViewGroup 就会自己处理点击事件。这就是 子元素如果没有消费这个事件,则事件又会向上传递给View。
如果事件被拦截,或者 出现上面这两种情况就会执行下面这个方法,如下所示:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
还记得 mFirstTouchTarget ,如果他等于 null ,说明了事件被拦截,或者 子View 的 dispatchTouchEvent 返回了false。看上面 的这段代码,又调用了 dispatchTransformedTouchEvent 方法,只不过 他的第三个参数为 null ,也就是子 View 为 null。还记得 dispatchTransformedTouchEvent 方法吗,在这个方法中,如果 子View 等于 null,则就会调用 super.dispatchTouchEvent 方法,即点击事件就交给 View 来处理了。
到这里 ViewGroup 的分发基本就完了,下面总结一下
1,如果是 DOWN 事件,且没有拦截,就会遍历这个ViewGroup 的所有 子View 。
2,如果子 View 满足 可见并没有播放动画 和 事件的坐标落在 子View 的区域内。那么事件就会传递给 子View
3,如果 子View 消费了事件,则直接影响 ViewGroup 对事件的拦截。如果子View 没有消费,这个事件就会传递给ViewGroup 父类View。如果 View 也没有消费,则事件就会从 ViewGroup 继续向上传递。
4,接上面第2点,如果子View 不满足 这两种情况,则会 退出本次循环,继续判断下一个View 是否满足。如果所有 子View 都不满足,则这个事件会传递给 ViewGroup的 父类的 dispatchTouchEvent 方法。如果父类也没有 消费,则事件 就会从 ViewGroup 继续向上传递。
5,接上面第 1 点,如果 DOWN 事件被 拦截,则 步骤 2,3,4 都不会发生,这个事件会直接传递给父类 View(类)。且事件被拦截后 DOWN 后面的所有事件 直到 UP 都会直接被拦截。,这个过程只会在 DOWN 的时候调用一次 onInterceptTouchEvent 方法,后续不会调用。
6,还记得 上面说的标记吗?这个标记可以在 子View 中 通过 requestDisallowInterceptTouchEvent 方法进行设置。设置后,ViewGroup 将无法拦截 DOWN 事件后的其他事件。但是 如果 ViewGroup 对事件进行了 拦截,导致事件没有分发 给 子View 。这种情况设置标记是没有用的。
3, View 对事件的处理过程。
首先看一下 dispatchTouchEvent 方法
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } ...... return result; }
因为这里 没有 子View 所以比较简单
首先,会判断 有没有设置 mOnTouchListener。看上面代码的 15 行,如果 onTouch 返回了true ,则 result 就为 true。 接着看 19 行,如果 result 为 true。则 onTouchEvent 就不会执行。只有 onTouch 返回 false 或者 没有设置 mOnTouchListener 的时候 onTouchEvent 才会执行。最后 将 result 返回给 调用者。
接着看一下 onTouchEvent 方法 对点击事件的处理
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; }
首先 判断 View 是否可用,显然,不可用的状态下 View 会销毁点击
接着 ,如果 View 有代理,那么 就会执行 mTouchDelegate 的 onTouchEvent 方法。
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
下面看一下 onTouchEvent 对点击事件的具体处理
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ....... boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ...... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 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)) { performClickInternal(); } } } ...... break; } return true; }
从代码来看,在 第一行,如果 clickable 和 TOOLTIP 有一个 为 true,那么就会消耗此事件。下面我们研究一下这个 clickable,如下所示
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
重点看一下 1 和 2 行,如果 CLICKBALE 或者 LONG_CLICKBAL 代表着 啥呢?
View 的 LONG_CLICKABLE 默认为 false ,而 CLICKABLE 是否为 false 和 具体的 View 有关,确切的说 是 可点击的 View 的 CLICKABLE 为 true,不可点击的 View 为 false。比如 Button 是可点击的。TextView 是不可点击的,通过 setClickable 和 setLongClickable 可以 改变 这两个属性 。另外 setOnClickListener 会自动将 VIiew 的CLICKABLE 设置为 true。setOnLongClickListener 会将 LONGCLICKABLE 设置 为 true。这一点 从源码可以看出来:
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; } public void setOnLongClickListener(@Nullable OnLongClickListener l) { if (!isLongClickable()) { setLongClickable(true); } getListenerInfo().mOnLongClickListener = l; }
当 UP 事件触发时,会 触发 performClickInternal在这个方法中 有 调用了 performClick 方法,如下:
public boolean performClick() { ...... final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
可以看到,如果mOnClickListener 不为 null ,就会调用 onClick 方法。
最后 就将 结果返回给 dispatchTouchEvent 方法,dispatchTouchEvent 就会将 结果返回给调用者, View 的 事件分发就 分析完了。下面总结一下
1,首先会判断有没有设置 onTouchListener,如果设置了 就看他的返回值。
2,返回值为 true 则消费此事件,事件到此结束。返回 false 则 继续调用 onTouchEvent 方法。
3,在 onTouchEvent 中 进行了 一系列的判断来确定 到底是 返回 true 还是 false。但是 在 onTouchEvent 中 判断了 onClickListener 是否被设置,如果设置了 就会调用他的点击事件。
4,如果 onTouchEvent 没有执行 ,则证明 onTouchListener.onTouch 反回了 true。
5,这两个 方法只要任意一个 返回 true ,则 当前事件被消费 返回 true。否则 返回 false,事件继续向上层传递。
到这里整个 事件体系就分析完了。下面参考一直别人的图,
从 Activity 开始 ,到 Window ,到 ViewGroup ,如果没有拦截 就分发给 子View ,子View 没有消费,返回了 false。这个false 就会 返回到 ViewGroup 的 onTouchEvent ,然后 一直向上传递,最终被 Activity 消费。
这个图只是一个大概,但是 他的 分发 和 回传的 流程是没有问题的。