自定义View系列教程07--详解ViewGroup分发Touch事件
2016-06-13 10:05
537 查看
深入探讨Android异步精髓Handler
站在源码的肩膀上全解Scroller工作机制
Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南
自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理
PS:如果觉得文章太长,那就直接看视频吧
在上一篇中已经分析完了View对于Touch事件的处理,在此基础上分析和理解ViewGroup对于Touch事件的分发就会相对容易些。
当一个Touch事件发生后,事件首先由系统传递给当前Activity并且由其dispatchTouchEvent()派发该Touch事件,源码如下:
该段代码主要逻辑如下:
处理ACTION_DOWN事件
调用onUserInteraction()该方法在源码中为一个空方法,可依据业务需求在Activity中覆写该方法。
利用PhoneWindow的superDispatchTouchEvent()派发事件
DecorView的superDispatchTouchEvent()源码如下:
此处我们可以看到:在该方法中又将Touch事件交给了DecorView进行派发。
DecorView继承自FrameLayout它是整个界面的最外层的ViewGroup。
至此,Touch事件就已经到了顶层的View且由其开始逐级派发。如果superDispatchTouchEvent()方法最终true则表示Touch事件被消费;反之,则进入下一步
Activity处理Touch事件
如果没有子View消费Touch事件,那么Activity会调用自身的onTouchEvent()处理Touch.
在以上步骤中第二步是我们关注的重点;它是ViewGroup对于Touch事件分发的核心。
关于dispatchTouchEvent(),请看如下源码:
第一步:
清理和还原状态,请参见代码第15-18行
ACTION_DOWN是一系列Touch事件的开端,当Touch为ACTION_DOWN时需要进行一些初始化和还原操作。比如:清除以往的Touch状态(state)和开始新的手势(gesture)。所以在cancelAndClearTouchTargets( )中将mFirstTouchTarget设置为null,且在resetTouchState()中重置Touch状态标识
第二步:
检查是否需要ViewGroup拦截Touch事件,请参见代码第20-31行
在此详细分析该段代码:
请注意变量intercepted,请参见代码第20行
该值用来标记ViewGroup是否拦截Touch事件的传递,它在后续代码中起着重要的作用.
事件为ACTION_DOWN或者mFirstTouchTarget不为null时检查是否需要ViewGroup拦截Touch事件,请参见代码第21行
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget !=null)
ACTION_DOWN表示Touch事件是手指按下的事件,那么mFirstTouchTarget又是什么意思呢?mFirstTouchTarget是TouchTarget类的对象,而TouchTarget是ViewGroup中的一个内部类,它封装了被触摸的View及这次触摸所对应的ID,该类主要用于多点触控。比如:三个指头依次按到了同一个Button上。
我们不必过多的理会TouchTarget,但是要重点关注mFirstTouchTarget。
mFirstTouchTarget贯穿dispatchTouchEvent(),对于流程的走向发挥着至关重要的作用。
(1) mFirstTouchTarget不为null
表示ViewGroup没有拦截Touch事件并且子View消费了Touch
(2) mFirstTouchTarget为null
表示ViewGroup拦截了Touch事件或者虽然ViewGroup没有拦截Touch事件但是子View也没有消费Touch。总之,此时需要ViewGroup自身处理Touch事件
如果ACTION_DOWN事件被子View消费(即mFirstTouchTarget!=null),当处理后续到来的ACTION_MOVE和ACTION_UP时仍会调用该代码判断是否需要拦截Touch事件。
2.1 判断disallowIntercept(禁止拦截)标志位,请参见代码第22行
ViewGroup可以拦截Touch事件,但是它的子View可调用 getParent().requestDisallowInterceptTouchEvent(true)禁止其父View的拦截。其实,从这个较长的方法名也可以看出来它的用途——禁止事件拦截;在该方法内部会改变FLAG_DISALLOW_INTERCEPT的值。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
所以利用此行代码判断是否禁止拦截(disallowIntercept)。
在此请注意:
ViewGroup中的requestDisallowInterceptTouchEvent( )方法可以用来禁止或允许ViewGroup拦截Touch事件,但是它对于ACTION_DOWN是无效的。
也就是说子View可以禁止父View拦截ACTION_MOVE和ACTION_UP但是无法禁止父View拦截ACTION_DOWN。因为在ACTION_DOWN时会调用resetTouchState()重置了FLAG_DISALLOW_INTERCEPT的值导致子View对该值设置失效。所以,对于ACTION_DOWN事件ViewGroup总会调用onInterceptTouchEvent()判读是否要拦截Touch事件
2.2 处理disallowIntercept的值为false的情况,请参见代码第23-25行
若disallowIntercept(禁止拦截)的值为false,所以调用onInterceptTouchEvent()拦截Touch并将结果赋值给intercepted。
常说ViewGroup的事件传递中的流程是:
dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent
其实在这就是一个体现:dispatchTouchEvent()中调用了onInterceptTouchEvent()。
2.3 处理disallowIntercept的值为true的情况,请参见代码第27行
若disallowIntercept(禁止拦截)的值为true,表示不拦截Touch事件。
所以将intercepted设置为false
将intercepted设置为true,请参见代码第30行
如果不是ACTION_DOWN事件并且mFirstTouchTarget为null,那么直接将intercepted设置为true,表示ViewGroup拦截Touch事件。
更加直白地说:如果ACTION_DOWN没有被子View消费(mFirstTouchTarget为null)那么当ACTION_MOVE和ACTION_UP到来时ViewGroup不再去调用onInterceptTouchEvent()判断是否需要拦截而是直接的将intercepted设置为true表示由其自身处理Touch事件
第三步:
检查cancel,请参见代码第38行
第四步:
分发ACTION_DOWN事件,请参见代码第43-117行
if (!canceled && !intercepted)
如果Touch事件没有被取消也没有被拦截,那么ViewGroup将类型为ACTION_DOWN的Touch事件分发给子View。
在此梳理该阶段的主要逻辑。
计算Touch事件的坐标,请参见代码第56-57行
在后续的判断中会依据坐标来判断触摸到了ViewGroup中的哪个子View。
依据坐标,判断哪个子View接收Touch事件,请参见代码第61-105行
这部分代码的主要操作为:
在找到可以接收Touch事件的子View后调用dispatchTransformedTouchEvent()方法将Touch事件派发给该子View。
第一种情况:
子View没有消费Touch事件则该方法的返回值为false,此时mFirstTouchTarget仍为null
第二种情况:
子View消费掉了Touch事件那么该方法的返回值为true,然后执行
newTouchTarget = addTouchTarget(child, idBitsToAssign);
在addTouchTarget()方法内将该子View添加到mFirstTouchTarget链表的表头,并且为mFirstTouchTarget设值使其不为null。随后将alreadyDispatchedToNewTouchTarget置为true,表示已经将Touch事件分发到了子View,或者说子View消费掉了Touch事件
小总结:
在这个步骤中只有找到了可以消费Touch事件的子View时mFirstTouchTarget才不为null;其余情况比如未找到可以接收Touch事件的子View或者子View不能消费Touch事件时mFirstTouchTarget仍为null
小疑惑:
请参见代码第78-82行:
为什么newTouchTarget!=null就会执行break跳出for循环了呢?
还记得这个for循环的作用是什么吗?——寻找一个可以接受Touch事件的子View。
如果先有个指头按在了子View上(即ACTION_DOWN),然后另一根指头又按在相同的子View上(即ACTION_POINTER_DOWN)。这种多点触摸的情况下两个指头按在了同一个View上,当第一指头按下的时候一个TouchTarget就已经记录了该子View,所以当第二个指头再按下的时候当然还是由这个子View来处理Touch事件,也就是说没有再继续寻找的必要了。
第五步:
继续事件分发,请参见代码第119-147行
在第四步对于ACTION_DOWN事件做了一些特有处理,在此继续进行事件的分发。不论是ACTION_DOWN还是ACTION_MOVE和ACTION_UP均会进入该步骤。
第一种情况:
mFirstTouchTarget==null
它表示Touch事件被ViewGroup拦截了根本就没有派发给子view或者虽然派发了但是在第四步中没有找到能够消费Touch事件的子View。
此时,直接调用dispatchTransformedTouchEvent()方法处理事件
第二种情况:
mFirstTouchTarget != null,表示找到了能够消费Touch事件的子View。
在该处亦有两种不同的情况:
处理ACTION_DOWN,请参见代码第126-127行
如果mFirstTouchTarget!=null则说明在第四步中Touch事件已经被消费,所以不再做其他处理
处理ACTION_MOVE和ACTION_UP,请参见代码第129-143行
调用dispatchTransformedTouchEvent()将事件分发给子View处理,请参见代码第130行
结合第四步和第五步,在此思考一个问题:
ViewGroup将ACTION_DOWN分发给子View,如果子View没有消费该事件,那么当ACTION_MOVE和ACTION_UP到来的时候系统还会将Touch事件派发给该子View么?
答案是否定的——如果子View没有处理ACTION_DOWN那么它就失去了处理ACTION_MOVE和ACTION_UP的资格。
在第四步中如果子View处理了ACTION事件那么mFirstTouchTarget不为null,当ACTION_MOVE和ACTION_UP到来时会跳过第四步进入到第五步。在第五步中就会判断mFirstTouchTarget是否为null,如果为空那么ViewGroup自身会处理Touch;如果不为空那么继续由mFirstTouchTarget处理Touch事件。
第六步:
清理数据和状态还原,请参见代码第149-155行
在手指抬起或者取消Touch分发时清除原有的相关数据
在分析dispatchTouchEvent()源码时多次调用dispatchTransformedTouchEvent(),在次对其源码做一个简略的分析
这段代码不算很复杂,先来瞅瞅官方文档的介绍
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.
该方法的主要作用是将Touch事件传递给特定的子View(即该方法的第三个输入参数child),由子Viwe继续分发处理Touch事件。
但我们发现:在系统调用dispatchTransformedTouchEvent()时该方法的第三个参数有时候是一个子View(比如dispatchTouchEvent()源码中第85和130行),有时候又是null(比如dispatchTouchEvent()源码中第120行).
那么该方法第三个参数child是否为null对于Touch事件的分发有什么影响呢?
在dispatchTransformedTouchEvent()源码中可见多次对于child是否为null的判断且均做出如下类似的操作:
child == null
如果子View没有消费掉Touch事件,那么ViewGroup就将自己动手处理Touch事件,即super.dispatchTouchEvent(event)。此时,ViewGroup就化身为了普通的View,它会在自己的onTouch(),onTouchEvent()中处理Touch;这个过程之前已经分析过了,不再赘述。
child != null
此时会调用该子View(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent()继续处理Touch,即child.dispatchTouchEvent(event)。
小结:
如果ViewGroup拦截了Touch事件或者子View不能消耗掉Touch事件,那么ViewGroup会在其自身的onTouch(),onTouchEvent()中处理Touch
如果子View消耗了Touch事件父View就不能再处理Touch.
至此我们就明白了:
Touch事件的传递顺序为
Activity–>外层ViewGroup–>内层ViewGroup–>View
Touch事件的消费顺序为
View–>内层ViewGroup–>外层ViewGroup–>Activity
其实,在我们平常的工作中也可以见到类似的场景。
开发任务的派发顺序为
CEO–>CTO–>manager–>developer
开发任务的反馈顺序为
developer–>manager–>CTO–>CEO
公司要做一个APP,CEO会将该任务交给CTO;CTO又找到了项目经理,最后项目经理将该任务分配给了开发人员。
在开发人员分析完项目后发现自己能力无法胜任于是就将该问题抛给了项目经理,项目经理觉得自己时间有限也完成不了又抛给了CTO,CTO同样因为某些因素无法按时完成该任务于是又将该任务抛给了CEO。
经过这么一圈折腾公司就觉得这个开发人员技术不是特别好,于是与该项目有关的后续工作也就不会再让这个开发人员参与了。这就像刚才提到的一样:如果子View没有消费ACTION_DOWN那么ACTION_MOVE和ACTION_UP也就不会再派发给它。
这个过程与ViewGroup对于Touch事件的分发是非常类似的。
至此,ViewGroup对于Touch事件的分发处理的主要流程就分析完了。
为了梳理整个dispatchTouchEvent()的脉络,我又画了两个流程图。
该流程图描述Touch事件的传递和消费顺序。
在Touch事件的传递过程中,如果上一级拦截了Touch那么其下一级就无法在收到Touch事件。
在Touch事件的消费过程中,如果下一级消费Touch事件那么其上一级就无法处理Touch事件。
该流程描述了dispatchTouchEvent( )中对于Touch分发。
这部分源码稍微有些复杂。结合此图,可以将整个流程分为三个阶段:
判断是否需要拦截(intercepted)
处理ACTION_DOWN事件
利用mFirstTouchTarget是否为null继续处理Touch(ACTION_DOWN,ACTION_MOVE,ACTION_UP)
嗯哼,源码分析完了,流程图也有了,我们再通过示例来验证和理解ViewGroup对于Touch事件的分发。
先来瞅瞅布局文件
此处,我们在示例的布局文件中放入了一个自定义的LinearLayout和Button。
先来看看这个自定义的线性布局LinearLayoutSubclass
在这个自定义的线性布局中主要是输出一些便于验证的日志信息,比如在dispatchTouchEvent()和onInterceptTouchEvent()以及onTouchEvent()中对于ACTION_DOWN和ACTION_MOVE以及ACTION_UP均输出对于信息。
再来看看自定义的按钮ButtonSubclass
在这个自定义Button中的处理和LinearLayoutSubclass非常类似只不过View是没有onInterceptTouchEvent()罢了,故此,不再赘述。
最后请看Activity的代码实现
在该Activity中除了日志输出亦无其余操作。
当手指轻触Button再抬起后,我们瞅瞅输出日志。
看到这些Log一切都是那么清晰明了
ACTION_DOWN事件由外及里从Activity传递到Button
Button处理了ACTION_DOWN事件
ACTION_UP事件由外及里从Activity传递到Button
Button处理了ACTION_UP事件
在这个过程中把Touch事件从Activity传递到最里层某个子View的过程体现得很清楚和完整。但是对于Touch事件的消费过程怎么没有体现出来呢?不是说Touch事件的消费顺序和传递过程是反过来的么?在这里怎么没有体现呢?
嗯哼,这是因为Button在onTouchEvent()中执行
return super.onTouchEvent(event);
消耗了Touch事件,所以Touch事件就没有回传给LinearLayoutSubclass和Activity。
现在对刚才的代码做一点小小的修改:
在ButtonSubclass的onTouchEvent()中返回false
在LinearLayoutSubclass的onTouchEvent()中返回false
现在再次运行代码并且轻触Button后抬起手指,观察一下输出日志:
ACTION_DOWN事件由外及里从Activity传递到Button。
Button没有处理ACTION_DOWN事件,将其回传至LinearLayoutSubclass
LinearLayoutSubclass也没有处理ACTION_DOWN事件,将其回传至Activty
Activity处理ACTION_DOWN
Activity处理ACTION_UP,不再分发
嗯哼,在这里就看明白了如果子View没有处理Touch事件就会回传给父View,一层一层地往上回溯。在刚才这个过程中没有一个子View处理ACTION_DOWN事件造成mFirstTouchTarget为null,所以当ACTION_UP事件发生时Activity不再将其派发给子View而是自己处理了。这个过程在之前分析源码的时候也着重提到过。
至此,关于ViewGroup对于Touch事件的分发就全部分析完了。
PS:如果觉得文章太长,那就直接看视频吧
who is the next one? ——> demo
站在源码的肩膀上全解Scroller工作机制
Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南
自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理
PS:如果觉得文章太长,那就直接看视频吧
在上一篇中已经分析完了View对于Touch事件的处理,在此基础上分析和理解ViewGroup对于Touch事件的分发就会相对容易些。
当一个Touch事件发生后,事件首先由系统传递给当前Activity并且由其dispatchTouchEvent()派发该Touch事件,源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
该段代码主要逻辑如下:
处理ACTION_DOWN事件
调用onUserInteraction()该方法在源码中为一个空方法,可依据业务需求在Activity中覆写该方法。
利用PhoneWindow的superDispatchTouchEvent()派发事件
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
DecorView的superDispatchTouchEvent()源码如下:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
此处我们可以看到:在该方法中又将Touch事件交给了DecorView进行派发。
DecorView继承自FrameLayout它是整个界面的最外层的ViewGroup。
至此,Touch事件就已经到了顶层的View且由其开始逐级派发。如果superDispatchTouchEvent()方法最终true则表示Touch事件被消费;反之,则进入下一步
Activity处理Touch事件
如果没有子View消费Touch事件,那么Activity会调用自身的onTouchEvent()处理Touch.
在以上步骤中第二步是我们关注的重点;它是ViewGroup对于Touch事件分发的核心。
关于dispatchTouchEvent(),请看如下源码:
public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } 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); } else { intercepted = false; } } else { intercepted = true; } if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = 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 actionIndex = ev.getActionIndex(); final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS; removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null&&isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder?getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null)?children[childIndex] : preorderedList.get(childIndex); if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child)||!isTransformedTouchPointInView(x,y,child,null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { 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; } ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS); } else { 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; } } 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; }
第一步:
清理和还原状态,请参见代码第15-18行
ACTION_DOWN是一系列Touch事件的开端,当Touch为ACTION_DOWN时需要进行一些初始化和还原操作。比如:清除以往的Touch状态(state)和开始新的手势(gesture)。所以在cancelAndClearTouchTargets( )中将mFirstTouchTarget设置为null,且在resetTouchState()中重置Touch状态标识
第二步:
检查是否需要ViewGroup拦截Touch事件,请参见代码第20-31行
在此详细分析该段代码:
请注意变量intercepted,请参见代码第20行
该值用来标记ViewGroup是否拦截Touch事件的传递,它在后续代码中起着重要的作用.
事件为ACTION_DOWN或者mFirstTouchTarget不为null时检查是否需要ViewGroup拦截Touch事件,请参见代码第21行
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget !=null)
ACTION_DOWN表示Touch事件是手指按下的事件,那么mFirstTouchTarget又是什么意思呢?mFirstTouchTarget是TouchTarget类的对象,而TouchTarget是ViewGroup中的一个内部类,它封装了被触摸的View及这次触摸所对应的ID,该类主要用于多点触控。比如:三个指头依次按到了同一个Button上。
我们不必过多的理会TouchTarget,但是要重点关注mFirstTouchTarget。
mFirstTouchTarget贯穿dispatchTouchEvent(),对于流程的走向发挥着至关重要的作用。
(1) mFirstTouchTarget不为null
表示ViewGroup没有拦截Touch事件并且子View消费了Touch
(2) mFirstTouchTarget为null
表示ViewGroup拦截了Touch事件或者虽然ViewGroup没有拦截Touch事件但是子View也没有消费Touch。总之,此时需要ViewGroup自身处理Touch事件
如果ACTION_DOWN事件被子View消费(即mFirstTouchTarget!=null),当处理后续到来的ACTION_MOVE和ACTION_UP时仍会调用该代码判断是否需要拦截Touch事件。
2.1 判断disallowIntercept(禁止拦截)标志位,请参见代码第22行
ViewGroup可以拦截Touch事件,但是它的子View可调用 getParent().requestDisallowInterceptTouchEvent(true)禁止其父View的拦截。其实,从这个较长的方法名也可以看出来它的用途——禁止事件拦截;在该方法内部会改变FLAG_DISALLOW_INTERCEPT的值。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
所以利用此行代码判断是否禁止拦截(disallowIntercept)。
在此请注意:
ViewGroup中的requestDisallowInterceptTouchEvent( )方法可以用来禁止或允许ViewGroup拦截Touch事件,但是它对于ACTION_DOWN是无效的。
也就是说子View可以禁止父View拦截ACTION_MOVE和ACTION_UP但是无法禁止父View拦截ACTION_DOWN。因为在ACTION_DOWN时会调用resetTouchState()重置了FLAG_DISALLOW_INTERCEPT的值导致子View对该值设置失效。所以,对于ACTION_DOWN事件ViewGroup总会调用onInterceptTouchEvent()判读是否要拦截Touch事件
2.2 处理disallowIntercept的值为false的情况,请参见代码第23-25行
若disallowIntercept(禁止拦截)的值为false,所以调用onInterceptTouchEvent()拦截Touch并将结果赋值给intercepted。
常说ViewGroup的事件传递中的流程是:
dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent
其实在这就是一个体现:dispatchTouchEvent()中调用了onInterceptTouchEvent()。
2.3 处理disallowIntercept的值为true的情况,请参见代码第27行
若disallowIntercept(禁止拦截)的值为true,表示不拦截Touch事件。
所以将intercepted设置为false
将intercepted设置为true,请参见代码第30行
如果不是ACTION_DOWN事件并且mFirstTouchTarget为null,那么直接将intercepted设置为true,表示ViewGroup拦截Touch事件。
更加直白地说:如果ACTION_DOWN没有被子View消费(mFirstTouchTarget为null)那么当ACTION_MOVE和ACTION_UP到来时ViewGroup不再去调用onInterceptTouchEvent()判断是否需要拦截而是直接的将intercepted设置为true表示由其自身处理Touch事件
第三步:
检查cancel,请参见代码第38行
第四步:
分发ACTION_DOWN事件,请参见代码第43-117行
if (!canceled && !intercepted)
如果Touch事件没有被取消也没有被拦截,那么ViewGroup将类型为ACTION_DOWN的Touch事件分发给子View。
在此梳理该阶段的主要逻辑。
计算Touch事件的坐标,请参见代码第56-57行
在后续的判断中会依据坐标来判断触摸到了ViewGroup中的哪个子View。
依据坐标,判断哪个子View接收Touch事件,请参见代码第61-105行
这部分代码的主要操作为:
在找到可以接收Touch事件的子View后调用dispatchTransformedTouchEvent()方法将Touch事件派发给该子View。
第一种情况:
子View没有消费Touch事件则该方法的返回值为false,此时mFirstTouchTarget仍为null
第二种情况:
子View消费掉了Touch事件那么该方法的返回值为true,然后执行
newTouchTarget = addTouchTarget(child, idBitsToAssign);
在addTouchTarget()方法内将该子View添加到mFirstTouchTarget链表的表头,并且为mFirstTouchTarget设值使其不为null。随后将alreadyDispatchedToNewTouchTarget置为true,表示已经将Touch事件分发到了子View,或者说子View消费掉了Touch事件
小总结:
在这个步骤中只有找到了可以消费Touch事件的子View时mFirstTouchTarget才不为null;其余情况比如未找到可以接收Touch事件的子View或者子View不能消费Touch事件时mFirstTouchTarget仍为null
小疑惑:
请参见代码第78-82行:
为什么newTouchTarget!=null就会执行break跳出for循环了呢?
还记得这个for循环的作用是什么吗?——寻找一个可以接受Touch事件的子View。
如果先有个指头按在了子View上(即ACTION_DOWN),然后另一根指头又按在相同的子View上(即ACTION_POINTER_DOWN)。这种多点触摸的情况下两个指头按在了同一个View上,当第一指头按下的时候一个TouchTarget就已经记录了该子View,所以当第二个指头再按下的时候当然还是由这个子View来处理Touch事件,也就是说没有再继续寻找的必要了。
第五步:
继续事件分发,请参见代码第119-147行
在第四步对于ACTION_DOWN事件做了一些特有处理,在此继续进行事件的分发。不论是ACTION_DOWN还是ACTION_MOVE和ACTION_UP均会进入该步骤。
第一种情况:
mFirstTouchTarget==null
它表示Touch事件被ViewGroup拦截了根本就没有派发给子view或者虽然派发了但是在第四步中没有找到能够消费Touch事件的子View。
此时,直接调用dispatchTransformedTouchEvent()方法处理事件
第二种情况:
mFirstTouchTarget != null,表示找到了能够消费Touch事件的子View。
在该处亦有两种不同的情况:
处理ACTION_DOWN,请参见代码第126-127行
如果mFirstTouchTarget!=null则说明在第四步中Touch事件已经被消费,所以不再做其他处理
处理ACTION_MOVE和ACTION_UP,请参见代码第129-143行
调用dispatchTransformedTouchEvent()将事件分发给子View处理,请参见代码第130行
结合第四步和第五步,在此思考一个问题:
ViewGroup将ACTION_DOWN分发给子View,如果子View没有消费该事件,那么当ACTION_MOVE和ACTION_UP到来的时候系统还会将Touch事件派发给该子View么?
答案是否定的——如果子View没有处理ACTION_DOWN那么它就失去了处理ACTION_MOVE和ACTION_UP的资格。
在第四步中如果子View处理了ACTION事件那么mFirstTouchTarget不为null,当ACTION_MOVE和ACTION_UP到来时会跳过第四步进入到第五步。在第五步中就会判断mFirstTouchTarget是否为null,如果为空那么ViewGroup自身会处理Touch;如果不为空那么继续由mFirstTouchTarget处理Touch事件。
第六步:
清理数据和状态还原,请参见代码第149-155行
在手指抬起或者取消Touch分发时清除原有的相关数据
在分析dispatchTouchEvent()源码时多次调用dispatchTransformedTouchEvent(),在次对其源码做一个简略的分析
private boolean dispatchTransformedTouchEvent(MotionEvent event,boolean cancel,View child,int desiredPointerIdBits) { final boolean handled; 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; } final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; if (newPointerIdBits == 0) { return false; } final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); } transformedEvent.recycle(); return handled; }
这段代码不算很复杂,先来瞅瞅官方文档的介绍
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.
该方法的主要作用是将Touch事件传递给特定的子View(即该方法的第三个输入参数child),由子Viwe继续分发处理Touch事件。
但我们发现:在系统调用dispatchTransformedTouchEvent()时该方法的第三个参数有时候是一个子View(比如dispatchTouchEvent()源码中第85和130行),有时候又是null(比如dispatchTouchEvent()源码中第120行).
那么该方法第三个参数child是否为null对于Touch事件的分发有什么影响呢?
在dispatchTransformedTouchEvent()源码中可见多次对于child是否为null的判断且均做出如下类似的操作:
if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); }
child == null
如果子View没有消费掉Touch事件,那么ViewGroup就将自己动手处理Touch事件,即super.dispatchTouchEvent(event)。此时,ViewGroup就化身为了普通的View,它会在自己的onTouch(),onTouchEvent()中处理Touch;这个过程之前已经分析过了,不再赘述。
child != null
此时会调用该子View(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent()继续处理Touch,即child.dispatchTouchEvent(event)。
小结:
如果ViewGroup拦截了Touch事件或者子View不能消耗掉Touch事件,那么ViewGroup会在其自身的onTouch(),onTouchEvent()中处理Touch
如果子View消耗了Touch事件父View就不能再处理Touch.
至此我们就明白了:
Touch事件的传递顺序为
Activity–>外层ViewGroup–>内层ViewGroup–>View
Touch事件的消费顺序为
View–>内层ViewGroup–>外层ViewGroup–>Activity
其实,在我们平常的工作中也可以见到类似的场景。
开发任务的派发顺序为
CEO–>CTO–>manager–>developer
开发任务的反馈顺序为
developer–>manager–>CTO–>CEO
公司要做一个APP,CEO会将该任务交给CTO;CTO又找到了项目经理,最后项目经理将该任务分配给了开发人员。
在开发人员分析完项目后发现自己能力无法胜任于是就将该问题抛给了项目经理,项目经理觉得自己时间有限也完成不了又抛给了CTO,CTO同样因为某些因素无法按时完成该任务于是又将该任务抛给了CEO。
经过这么一圈折腾公司就觉得这个开发人员技术不是特别好,于是与该项目有关的后续工作也就不会再让这个开发人员参与了。这就像刚才提到的一样:如果子View没有消费ACTION_DOWN那么ACTION_MOVE和ACTION_UP也就不会再派发给它。
这个过程与ViewGroup对于Touch事件的分发是非常类似的。
至此,ViewGroup对于Touch事件的分发处理的主要流程就分析完了。
为了梳理整个dispatchTouchEvent()的脉络,我又画了两个流程图。
该流程图描述Touch事件的传递和消费顺序。
在Touch事件的传递过程中,如果上一级拦截了Touch那么其下一级就无法在收到Touch事件。
在Touch事件的消费过程中,如果下一级消费Touch事件那么其上一级就无法处理Touch事件。
该流程描述了dispatchTouchEvent( )中对于Touch分发。
这部分源码稍微有些复杂。结合此图,可以将整个流程分为三个阶段:
判断是否需要拦截(intercepted)
处理ACTION_DOWN事件
利用mFirstTouchTarget是否为null继续处理Touch(ACTION_DOWN,ACTION_MOVE,ACTION_UP)
嗯哼,源码分析完了,流程图也有了,我们再通过示例来验证和理解ViewGroup对于Touch事件的分发。
先来瞅瞅布局文件
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.stay4it.testtouch1.MainActivity"> <com.stay4it.testtouch1.LinearLayoutSubclass android:layout_width="match_parent" android:layout_height="match_parent"> <com.stay4it.testtouch1.ButtonSubclass android:id="@+id/button" android:layout_centerInParent="true" android:layout_width="500px" android:layout_height="500px" android:background="#FF3A9120" android:textSize="50px" android:text="Touch Me" /> </com.stay4it.testtouch1.LinearLayoutSubclass> </RelativeLayout>
此处,我们在示例的布局文件中放入了一个自定义的LinearLayout和Button。
先来看看这个自定义的线性布局LinearLayoutSubclass
package com.stay4it.testtouch1; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.LinearLayout; /** * 原创作者 * 谷哥的小弟 * * 博客地址 * http://blog.csdn.net/lfdfhl */ public class LinearLayoutSubclass extends LinearLayout { public LinearLayoutSubclass(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> LinearLayoutSubclass中调用dispatchTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> LinearLayoutSubclass中调用dispatchTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> LinearLayoutSubclass中调用dispatchTouchEvent()--->ACTION_UP"); default: break; } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> LinearLayoutSubclass中调用onInterceptTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> LinearLayoutSubclass中调用onInterceptTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> LinearLayoutSubclass中调用onInterceptTouchEvent()--->ACTION_UP"); default: break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> LinearLayoutSubclass中调用onTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> LinearLayoutSubclass中调用onTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> LinearLayoutSubclass中调用onTouchEvent()--->ACTION_UP"); default: break; } return super.onTouchEvent(ev); } }
在这个自定义的线性布局中主要是输出一些便于验证的日志信息,比如在dispatchTouchEvent()和onInterceptTouchEvent()以及onTouchEvent()中对于ACTION_DOWN和ACTION_MOVE以及ACTION_UP均输出对于信息。
再来看看自定义的按钮ButtonSubclass
package com.stay4it.testtouch1; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.Button; /** * 原创作者 * 谷哥的小弟 * * 博客地址 * http://blog.csdn.net/lfdfhl */ public class ButtonSubclass extends Button{ public ButtonSubclass(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> ButtonSubclass中调用dispatchTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> ButtonSubclass中调用dispatchTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> ButtonSubclass中调用dispatchTouchEvent()--->ACTION_UP"); default: break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> ButtonSubclass中调用onTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> ButtonSubclass中调用onTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> ButtonSubclass中调用onTouchEvent()--->ACTION_UP"); default: break; } return super.onTouchEvent(event); } }
在这个自定义Button中的处理和LinearLayoutSubclass非常类似只不过View是没有onInterceptTouchEvent()罢了,故此,不再赘述。
最后请看Activity的代码实现
package com.stay4it.testtouch1; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.MotionEvent; /** * 原创作者 * 谷哥的小弟 * * 博客地址 * http://blog.csdn.net/lfdfhl */ public class MainActivity extends AppCompatActivity { private ButtonSubclass mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init(){ mButton= (ButtonSubclass) findViewById(R.id.button); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> MainActivity中调用dispatchTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> MainActivity中调用dispatchTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> MainActivity中调用dispatchTouchEvent()--->ACTION_UP"); default: break; } return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("---> MainActivity中调用onTouchEvent()--->ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("---> MainActivity中调用onTouchEvent()--->ACTION_MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("---> MainActivity中调用onTouchEvent()--->ACTION_UP"); default: break; } return super.onTouchEvent(ev); } }
在该Activity中除了日志输出亦无其余操作。
当手指轻触Button再抬起后,我们瞅瞅输出日志。
看到这些Log一切都是那么清晰明了
ACTION_DOWN事件由外及里从Activity传递到Button
Button处理了ACTION_DOWN事件
ACTION_UP事件由外及里从Activity传递到Button
Button处理了ACTION_UP事件
在这个过程中把Touch事件从Activity传递到最里层某个子View的过程体现得很清楚和完整。但是对于Touch事件的消费过程怎么没有体现出来呢?不是说Touch事件的消费顺序和传递过程是反过来的么?在这里怎么没有体现呢?
嗯哼,这是因为Button在onTouchEvent()中执行
return super.onTouchEvent(event);
消耗了Touch事件,所以Touch事件就没有回传给LinearLayoutSubclass和Activity。
现在对刚才的代码做一点小小的修改:
在ButtonSubclass的onTouchEvent()中返回false
在LinearLayoutSubclass的onTouchEvent()中返回false
现在再次运行代码并且轻触Button后抬起手指,观察一下输出日志:
ACTION_DOWN事件由外及里从Activity传递到Button。
Button没有处理ACTION_DOWN事件,将其回传至LinearLayoutSubclass
LinearLayoutSubclass也没有处理ACTION_DOWN事件,将其回传至Activty
Activity处理ACTION_DOWN
Activity处理ACTION_UP,不再分发
嗯哼,在这里就看明白了如果子View没有处理Touch事件就会回传给父View,一层一层地往上回溯。在刚才这个过程中没有一个子View处理ACTION_DOWN事件造成mFirstTouchTarget为null,所以当ACTION_UP事件发生时Activity不再将其派发给子View而是自己处理了。这个过程在之前分析源码的时候也着重提到过。
至此,关于ViewGroup对于Touch事件的分发就全部分析完了。
PS:如果觉得文章太长,那就直接看视频吧
who is the next one? ——> demo
相关文章推荐
- 自定义View系列教程07--详解ViewGroup分发Touch事件
- 自定义View系列教程07--详解ViewGroup分发Touch事件
- Eclipse Android项目开发完成以后就要将android项目文件打包成apk文件
- if中的判断条件
- RedHat server6.3/centos6.5系统配置本地yum源并安装图形界面
- android5.1 蓝牙上电流程
- ion-content 滚动到底部会遮住一部分视图的解决办法
- 解决firefox一个邮箱两个账户导致收藏夹不同步的问题
- objective-C之NSDate相关类(二)
- 关于Application.Lock…Application.Unlock有什么作用?
- 关于Application.Lock和Lock(obj)
- 面向对象(一)
- linux下利用split分割大文件
- .9作图
- python的文件操作说明
- Mysql存储引擎中InnoDB与Myisam的区别
- LRC的效验码的计算方法
- python 字符串的split()函数详解
- 程序员为什么要时刻保持危机感?
- Rectangle Area