内容简介:在进行正文之前,我们带着以下几个问题有目的的进行,然后最后再做问题的解决。首先我们要清楚,事件分发的对象是什么?其实就 MotionEvent,这个 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件类型。Activity -> Window -> ViewGroup -> View
在进行正文之前,我们带着以下几个问题有目的的进行,然后最后再做问题的解决。
- 问题 1:activity、 ViewGroup和 View 都不消费 ACTION_DOWN,那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?
- 问题 2:在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?
- 在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?
- 为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了。
基础认识
事件分发的对象
首先我们要清楚,事件分发的对象是什么?其实就 MotionEvent,这个 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件类型。
事件(MotionEvent)分发是在哪些对象中进行的?
Activity -> Window -> ViewGroup -> View
重点关注的方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
ACTION_DOWN 事件在 Activity、ViewGroup、View 中根据不同函数不同的返回值的走向?
关于事件的走向,我觉的以下这张图可以很清晰的看出事件的最终走向,该图来自Kelin
注:流程图来之Kelin以下是使用文字对事件走向的描述,帮助对流程图的理解。
方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | (1) return false 或者是 return true,表示 Activity 已经消费 (2) return 父类的 super.dispatchTouchEvent() 表示向下传递,最后结果由下一级决定 |
(1) return false,表示 ViewGroup 不消费事件,返回给上一级的 ViewGroup 或者 Activity 的 onTouchEvent 进行处理; (2) return true,表示当前 ViewGroup 已经消费了事件, 事件在此终止 ; (3) return super.dispatchTouchEvent(),表示事件继续,根据 onInterceptTouchEvent 判断是否自己处理事件(是否调用 onTouchEvent) |
(1) return false,表示 View 不消费事件,返回给上一级的 ViewGroup onTouchEvent 进行处理; (2) return true,表示当前 View 已经消费了事件, 事件在此终止 ; (3) return super.dispatchTouchEvent(),表示事件继续,并调用自己的方法 onTouchEvent |
onInterceptTouchEvent | 没有此方法 | (1) return true,表示 ViewGroup 拦截该事件;之后 onInterceptTouchEvent 不会再被调用 (2) return false 或者 return super.onInterceptTouchEvent 表示不拦截事件,将事件交给子 view 处理。 |
没有此方法 |
onTouchEvent | (1) return false,表示所有的 ViewGroup 和 View 都不消费事件,Activity 也不消费; (2) return true,表示消费该事件, 事件在此终止 。 |
(1) return false 或者是 return super.onTouchEvent 表示不消费事件,返回给上一级的 onTouchEvent 处理 (2) return true 表示当前 ViewGroup 自己消费了, 事件在此终止 ,不会往上传也不会往下传了。 |
(1) return false 或者是 return super.onTouchEvent 表示不消费事件,返回给上一级的 ViewGroup 进行护理; (2) return true 表示当前 view 进行处理事件, 事件在此终止 |
结论
- dispatchTouchEvent 和 onTouchEvent 中返回 true,ACTION_DOWN 事件就在此终止,不会再往上传也不会往下传了。
- dispatchTouchEvent 和 onTouchEvent 中返回 false,ACTION_DOWN 事件交给父控件的 onTouchEvent 进行处理
- onInterceptTouchEvent 一旦返回 true,那么 ViewGroup 之后不会再调用 onInterceptTouchEvent
事件分发源码分析
Activity
一旦事件产生,那么首先被调用的是 Activity 中的 dispatchTouchEvent 方法。我们来看看这个方法的实现。
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } 复制代码
首先,如果是 ACTION_DOWN,那么 onUserInteraction 方法就会被调用,onUserInteraction 是个空的方法,当事件产生时,那么这个方法就会被调用,如果在 activity 运行的时候,我们想要知道用户和设备的交互,那么我们就可以实现这个方法。
接着 window 中的 superDispatchTouchEvent 方法被调用,事件传递到 window 中。Window 是个抽象类,他的唯一实现是 PhoneWindow。在 Window.superDispatchTouchEvent 中调用了 DecorView 的 superDispatchTouchEvent,而这个 DecorView 就是我们在 activity 中通过调用 setContentView 设置的布局的顶层 View。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 复制代码
最后,如果 Activity 中的所有下级事件承载对象没有处理事件,最后 Activity 中的 onTouchEvent 就会被调用,当事件超出边界或者事件为 ACTION_DOWN 时,mWindow.shouldCloseOnTouch(this, event) 为 true,onTouchEvent 默认是返回 false 的。
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; } 复制代码
onTouchEvent 方法当所有的 view 都没有消费事件的时候,activity 的 onTouchEvent() 就会被调用
我们这边看一下 getWindow() 里面的实现,其实也就是返回一个 Window 实例
public Window getWindow() { return mWindow; } 复制代码
Window(PhoneWindow)
Window 在事件分发的过程中就类似于一个中间的桥接一样,是没有做什么操作的,只是将事件传递给 DecorView 中。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 复制代码
ViewGroup
- 要理解 ViewGroup 对于事件的处理,我们先来个简化的代码逻辑来帮助我们理解
void dispathTouchEvent(Event event){ boolean consume = false; if(OnInterceptTouchEvent(event)){ consume = onTouchEvent(event); }else{ consume = childView.dispatchTouchEvent(event); } return consume; } 复制代码
对于一个 ViewGroup 来说,点击事件产生之后,dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要拦截当前的事件,接着事件就会给这个 ViewGroup 处理,即它的 onTouchEvent 会被调用;如果 ViewGroup 的 onInterceptTouchEvent 返回 false,就表示它不拦截事件,这时当前事件就会被传递给它的子 view,接着调用子元素的 dispatchTouchEvent 方法就会被调用,如此反复,直到事件被最终处理。
- 大致流程图
- 我们再看看 ViewGroup.dispatchTouchEvent() 还原度比较高的源码
// 省略部分代码 // 如果是 ACTION_DOWN 事件,重置标志位 mGroupFlags 为 非 FLAG_DISALLOW_INTERCEPT,这个标志位关系到 ViewGroup 的 onInterceptTouch 是否有效。 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 检查是否拦截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 处理条件为: // 1. 事件为 ACTION_DOWN // 2. 有下级的 View 处理事件 // 判断拦截是否失效?mGroupFlags = FLAG_DISALLOW_INTERCEPT 时,onInterceptTouchEvent 是不会被调用的 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 { // 非 ACTION_DOWN 事件且没有其他的下级处理该事件的时候,不会再调用 onInterceptTouchEvent intercepted = true; } // 正常事件分发 // 如果 ViewGroup 决定拦截或者已经有 子 view 处理事件,那么就开始正常的事件分发流程 if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // 不拦截事件 if (!canceled && !intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { 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); // 找到接收该事件的子 View 上,如果找到,则直接跳出循环 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { 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); } if (preorderedList != null) preorderedList.clear(); } } } 复制代码
从以上的代码中可以看出,当事件传递到 ViewGroup 中的 dispatchTouchEvent 的时候,这次经历了以下的几个重要步骤:
- step 1: 重置标志位 mGroupFlags,将 mGroupFlags 标志位设置为非 FLAG_DISALLOW_INTERCEPT,如果 mGroupFlags = FLAG_DISALLOW_INTERCEPT,那么 ViewGroup 将不再调用 onInterceptTouch(),默认 ViewGroup 不拦截任何事件。
- step 2: 通过判断事件是否为 ACTION_DOWN 或 mFirstTouchTarget != null 来决定是否向下分发 ACTION_DOWN 之外的事件;
- 若是子 View 消费 ACTION_DOWN,那么 mFirstTouchTarget 会被赋值,mFirstTouchTarget != null 不成立
- 若是子 View 不消费 ACTION_DOWN,那么 mFirstTouchTarget 则为 null,ViewGroup 默认拦截 ACTION_DOWN 之后的所有事件,不向下传递。
- step 3:通过 mGroupFlags 标志位判断拦截是否有效,若是 mGroupFlags = FLAG_DISALLOW_INTERCEPT,则 ViewGroup 默认不拦截任何事件。
- step 4: 循环所有的子 view
- (1)判断是否有子 view 已经处理改事件了,如果有则跳出循环,直接向下级子 view 分发事件。
- (2)判断点击是否落在某个子 view;
- step 5: 如果子 view 消费了事件,那么将 mFirstTouchTarget 进行赋值,mFirstTouchTarget(链表)。
View
View 对于事件的处理要稍微简单一点,注意这里的 View 并不包含 ViewGroup。我们先看看 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; } } ... return result; } 复制代码
View 对于点击事件的处理就比较简单了,因为 View 是个单独的元素,它没有子元素,因此无法向下传递事件,所以它只能自己处理。
从上面的源码中,我们可以看出 View 对点击事件的处理过程,首先会判断有没有设置 onTouchListener,如果 onTouchListener 中的 onTouch 返回了 true,那么 onTouchEvent 就不会再被调用,可见 onTouchListener 的优先级要高于 onTouchEvent,这样的处理是方便点击事件在外界进行处理。
- View 中的 onTouchEvent 的实现
public boolean onTouchEvent(MotionEvent event) { // 不可用状态下,View 依然会消耗点击事件 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; return clickable; } // 如果设置了代理,那么就设置代理的方法 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 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(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } .... } break; } return true; } return false; } 复制代码
从上面的代码中可以看出,只要 View 的 CLICKABLE 和 LONG_CLICK 有一个为 true,那么它就会消耗这个事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 状态。
然后当 ACTION_UP 事件发生的时候,会触发 performClick 方法,如果 View 设置了 onClickListener,那么 performClick 方法内部就会调用它的 onClick 方法。如下所示:
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; } 复制代码
到这里,点击事件的源码分析就结束了。
问题解答
问题 1:activity、 ViewGroup和 View 都不消费 ACTION_DOWN,那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?
- 首先,如果大家都不消费 ACTION_DOWN,那么 ACTION_DOWN 的事件传递流程是这样的:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent() -> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent() -> Activity.onTouchEvent(); 复制代码
- 接着,由于大家都不消费 ACTION_DOWN,对于 ACTION_MOVE 和 ACTION_UP 的事件传递是这样的
-> Activity.dispatchTouchEvent() -> Activity.onTouchEvent(); -> 消费 复制代码
- 完整的事件分发走向
问题 2:在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?
- 首先,我们先分析一下 ACTION_DOWN 的事件走向,由于 ViewGroup 中的 onInterceptTouch 是默认设置的,那么 ACTION_DOWN 的事件最终在 ViewGroup 中的 onTouchEvent 方法中停止了,事件走向是这样的:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent() -> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent() 复制代码
- 接着 ACTION_MOVE 和 ACTION_UP 的事件分发流程,之后 onInterceptTouch 和 View 中的方法都不会被调用了,事件分发如下:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onTouchEvent() 复制代码
- 完整的事件分发走向
在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?
答案是优先响应子 view,原因很简单,如果先响应父 view,那么子 view 将永远无法响应,父 view 要优先响应事件,必须先调用 onInterceptTouchEvent 对事件进行拦截,那么事件不会再往下传递,直接交给父 view 的 onTouchEvent 处理。
为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了。
答案是:mFirstTouchTarget。当子 view 对事件进行处理的时,那么 mFirstTouchTarget 就会被赋值,若是子 view 不对事件进行处理,那么 mFirstTouchTarget 就为 null,之后 VIewGroup 就会默认拦截所有的事件。我们可以从 dispatchTouchEvent 中找到如下代码,可以看出来,若是子 View 不处理 ACTION_DOWN,那么之后的事件也不会给到它了。
// 检查是否拦截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 省略和问题无关代码 } else { // 默认拦截 intercepted = true; } 复制代码
以上所述就是小编给大家介绍的《Android 事件分发机制的理解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Android事件分发机制
- View事件分发机制分析
- View的事件分发机制【原创】
- 面试系列之事件分发机制全解析
- Android事件分发机制[-View-] 源码级
- Flex 事件分发(FlexViewer事件机制)剥离过程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
产品经理面试宝典
[美] Gayle Laakmann McDowell、[美]Jackie Bavaro / 吴海星、陈少芸 / 人民邮电出版社 / 2015-3 / 59.00元
本书针对IT 行业产品经理,以面试为主线,首先介绍产品经理职责以及谷歌、微软等知名企业中产品经理的作用和要求;然后采访了几位知名企业的产品经理,介绍成为产品经理的基本素质;之后从简历准备、各公司面试要点到具体面试问题进行详细分析,这部分是本书的重点内容。读者对象包括IT 行业产品经理以及对如何做好产品有兴趣的人士。一起来看看 《产品经理面试宝典》 这本书的介绍吧!