很高兴碰见你~css
在上一篇文章 Android事件分发机制一:事件是如何到达activity的? 中,咱们讨论了触摸信息从屏幕产生到发送给具体 的view处理的总体流程,这里先来简单回顾一下:java
前面的分发步骤咱们清楚了,那么viewGroup是如何对触摸事件进行分发的呢?View又是如何处理触摸信息的呢?正是本文要讨论的内容。android
事件处理中涉及到的关键方法就是 dispatchTouchEvent
,无论是viewGroup仍是view。在viewGroup中,dispatchTouchEvent
方法主要是把事件分发给子view,而在view中,dispatchTouchEvent
主要是处理消费事件。而主要的消费事件内容是在 onTouchEvent
方法中。下面讨论的是viewGroup与view的默认实现,而在自定义view中,一般会重写 dispatchTouchEvent
和 onTouchEvent
方法,例如DecorView等。git
秉着逻辑先行源码后到的原则,本文虽然涉及到大量的源码,但会优先讲清楚流程,有时间的读者仍然建议阅读完整源码。面试
事件分发中涉及到一个很重要的点:多点触控,这是在不少的文章中没有体现出来的。而要理解viewGroup如何处理多点触控,首先须要对触摸事件信息类:MotionEvent,有必定的认识。MotionEvent中承载了触摸事件的不少信息,理解它更有利于咱们理解viewGroup的分发逻辑。因此,首先须要先理解MotionEvent。api
触摸事件的基本类型有三种:数组
一个完整的触摸事件系列是:从ACTION_DOWN开始,到ACTION_UP结束 。这其实很好理解,就是手指按下开始,手指抬起结束。安全
手指可能会在屏幕上滑动,那么中间会有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE...、ACTION_UP。并发
这是正常的状况,而若是出现了一些异常的状况,事件序列被中断,那么会产生一个取消事件:ide
因此,完整的事件序列是:从ACTION_DOWN开始,到ACTION_UP或者ACTION_CANCEL结束 。固然,这是咱们一个手指的状况,那么在多指操做的状况是怎么样的呢?这里须要引入另外的事件类型:
区别于ACTION_DOWN和ACTION_UP,使用另外两个事件类型来表示手指的按下与抬起,使得ACTION_DOWN和ACTION_UP能够做为一个完整的事件序列的边界 。
同时,一个手指的事件序列,是从ACTION_DOWN/ACTION_POINTER_DOWN开始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL结束。
到这里先简单作个小结:
触摸事件的类型有:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_DOWN、ACTION_POINTER_UP,他们分别表明不一样的场景。
一个完整的事件序列是从ACTION_DOWN开始,到ACTION_UP或者ACTION_CANCEL结束。
一个手指的完整序列是从ACTION_DOWN/ACTION_POINTER_DOWN开始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL结束。
第二,咱们须要理解MotionEvent中所携带的信息。
假如如今屏幕上有两个手指按下,以下图:
触摸点a先按下,而触摸点b后按下,那么天然而然就会产生两个事件:ACTION_DOWN和ACTION_POINTER_DOWN。那么是否是ACTION_DOWN事件就只包含有触摸点a的信息,而ACTION_POINTER_DOWN只包含触摸点b的信息呢?换句话说,这两个事件是否是会独立发出触摸事件?答案是:不是。
每个触摸事件中,都包含有全部触控点的信息。例如上述的点b按下时产生的ACTION_POINTER_DOWN事件中,就包含了触摸点a和触摸点b的信息。那么他是如何区分这两个点的信息?咱们又是如何知道ACTION_POINTER_DOWN这个事件类型是属于触摸点a仍是触摸点b?
在MotionEvent对象内部,维护有一个数组。这个数组中的每一项对应不一样的触摸点的信息,以下图:
数组下标称为触控点的索引,每一个节点,拥有一个触控点的完整信息。这里要注意的是,一个触控点的索引并非一成不变的,而是会随着触控点的数目变化而变化。例如当同时按下两个手指时,数组状况以下图:
而当手指a抬起后,数组的状况变为下图:
能够看到触控点b的索引改变了。因此跟踪一个触控点必须是依靠一个触控点的id,而不是他的索引 。
如今咱们知道每个MotionEvent内部都维护有全部触控点的信息,那么咱们怎么知道这个事件是对应哪一个触控点呢?这就须要看到MotionEvent的一个方法:getAction
。
这个方法返回一个整型变量,他的低1-8位表示该事件的类型,高9-16位表示触控点索引。咱们只须要将这16位进行分离,就能够知道触控点的类型和所对应的触控点。同时,MotionEvent有两个获取触控点坐标的方法:getX()/getY()
,他们都须要传入一个触控点索引来表示获取哪一个触控点的坐标信息。
同时还要注意的是,MOVE事件和CANCEL事件是没有包含触控点索引的,只有DOWN类型和UP类型的事件才包含触控点索引。这里是由于非DOWN/UP事件,不涉及到触控点的增长与删除。
这里咱们再来小结一下:
- 一个MotionEvent对象内部使用一个数组来维护全部触控点的信息
- UP/DOWN类型的事件包含了触控点索引,能够根据该索引作出对应的操做
- 触控点的索引是变化的,不能做为跟踪的依据,而必须依据触控点id
关于MotionEvent须要了解一个更加剧要的点:事件分离。
首先须要知道事件分发的一个原则:一个view消费了某一个触点的down事件后,该触点事件序列的后续事件,都由该view消费 。这也比较符合咱们的操做习惯。当咱们按下一个控件后,只要咱们的手指一直没有离开屏幕,那么咱们但愿这个手指滑动的信息都交给这个view来处理。换句话说,一个触控点的事件序列,只能给一个view消费。
通过前面的描述咱们知道,一个事件是包含全部触摸点的信息的。当viewGroup在派发事件时,每一个触摸点的信息就须要分开分别发送给感兴趣的view,这就是事件分离。
例如Button1接收了触摸点a的down事件,Button2接收了触摸点b的down事件,那么当一个MotionEvent对象到来时,须要将他里面的触摸点信息,把触摸点a的信息拆开发送给button1,把触摸点b的信息拆开发送给button2。以下图:
那么,可不能够不进行分离?固然能够。这样的话每次都把全部触控点的信息发送给子view。这能够经过FLAG_SPLIT_MOTION_EVENTS这个标志进行设置是否要进行分离。
小结一下:
一个触控点的序列通常状况下只给一个view处理,当一个view消费了一个触控点的down事件后,该触控点的事件序列后续事件都会交给他处理。
事件分离是把一个motionEvent中的触控点信息进行分离,只向子view发送其感兴趣的触控点信息。
咱们能够经过设置FLAG_SPLIT_MOTION_EVENTS标志让viewGroup是否对事件进行分离
到这里关于MotionEvent的内容就讲得差很少,固然在分离的时候,还须要进行必定的调整,例如坐标轴的更改、事件类型的更改等等,放在后面讲,接下来看看ViewGroup是如何分发事件的。
这一步能够说是事件分发中的重头戏了。不过在理解了上面的MotionEvent以后,对于ViewGroup的分发细节也就容易理解了。
总体来讲,ViewGroup分发事件分为三个大部分,后面的内容也会围绕着三大部分展开:
大致的流程是:每个事件viewGroup会先判断是否要拦截,若是是down事件(这里的down事件表示ACTION_DOWN和ACTION_POINTER_DOWN,下同),还须要挨个遍历子view看看是否有子view消费了down事件,最后再把事件派发下去。
在开始解析以前,必须先了解一个关键对象:TouchTarget。
前面咱们讲到:一个触控点的序列通常状况下只给一个view处理,当一个view消费了一个触控点的down事件后,该触控点的事件序列后续事件都会交给他处理。对于viewGroup来讲,他有不少个子view,若是不一样的子view接受了不一样的触控点的down事件,那么ViewGroup如何记录这些信息并精准把事件发送给对应的子view呢?答案就是:TouchTarget。
TouchTarget中维护了每一个子view以及所对应的触控点id,这里的id能够不止一个。TouchTarget自己是个链表,每一个节点记录了子view所对应的触控点id。在viewGroup中,该链表的链表头是mFirstTouchTarget,若是他为null,表示没有任何子view接收了down事件。
TouchTarget有个很是神奇的设计,他只使用一个整型变量来记录全部的触控id。整型变量中哪个二进制位为1,则对应绑定该id的触控点。
例如 00000000 00000000 00000000 10001000,则表示绑定了id为3和id为7的两个触控点,由于第3位和第7位的二进制位是1。这里能够间接说明系统支持的最大多点触控数是32,固然实际上通常是8比较多。当要判断一个TouchTarget绑定了哪些id时,只须要经过必定的位操做便可,既提升了速度,也优化了空间占用。
当一个down事件来临时,viewGroup会为这个down事件寻找适合的子view,并为他们建立一个TouchTarget加入到链表中。而当一个up事件来临时,viewGroup会把对应的TouchTarget节点信息删除。那接下来,就直接看到viewGroup中的dispatchTouchEvent
是如何分发事件的。首先看到源码中的第一部分:事件拦截。
这里的拦截分为两部分:安全拦截和逻辑拦截。
安全拦截是一直被忽略的一种状况。当一个控件a被另外一个非全屏控件b遮挡住的时候,那么有可能被恶意软件操做发生危险。例如咱们看到的界面是这样的:
但实际上,咱们看到的这个按钮时不可点击的,实际上触摸事件会被分发到这个按钮后面的真正接收事件的按钮:
而后咱们就白给了。这个安全拦截行为由两个标志控制:
具体的源码以下:
View.java api29 public boolean onFilterTouchEventForSecurity(MotionEvent event) { // 两个标志,前者表示当被覆盖时不处理;后者表示当前窗口是否被非全屏窗口覆盖 if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true; }
第二种拦截是逻辑拦截。若是当前viewGroup中没有TouchTarget,并且这个事件不是down事件,这就意味着viewGroup本身消费了先前的down事件,那么这个事件就无须分发到子view必须本身消费,也就不须要拦截这种状况的事件。除此以外的事件都是须要分发到子view,那么viewGroup就能够对他们进行判断是否进行拦截。简单来讲,只有须要分发到子view的事件才须要拦截 。
判断是否拦截主要依靠两个因素:FLAG_DISALLOW_INTERCEPT标志和 onInterceptTouchEvent()
方法。
onInterceptTouchEvent()
方法判断是否须要拦截。onInterceptTouchEvent方法默认只对一种特殊状况做了拦截。通常状况下咱们会重写这个方法来拦截事件:// 只对一种特殊状况作了拦截 // 鼠标左键点击了滑动块 public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
viewGroup的 dispatchTouchEvent
方法逻辑中对于事件拦截部分的源码分析以下:
ViewGroup.java api29 public boolean dispatchTouchEvent(MotionEvent ev) { ... // 对遮盖状态进行过滤 if (onFilterTouchEventForSecurity(ev)) { ... // 判断是否须要拦截 final boolean intercepted; // down事件或者有target的非down事件则须要判断是否须要拦截 // 不然不须要进行拦截判断,由于必定是交给本身处理 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 此标志为子view经过requestDisallowInterupt方法设置 // 禁止viewGroup拦截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 调用onInterceptTouchEvent判断是否须要拦截 intercepted = onInterceptTouchEvent(ev); // 恢复事件状态 ev.setAction(action); } else { intercepted = false; } } else { // 本身消费了down事件,那么后续的事件非down事件都是本身处理 intercepted = true; } ...; } ...; }
对于每个down事件,无论是ACTION_DOWN仍是ACTION_POINTER_DOWN,viewGroup都会优先在控件树中寻找合适的子控件来消费他。由于对于每个down事件,标志着一个触控点的一个崭新的事件序列,viewGroup会尽本身的最大能力寻找合适的子控件。若是找不到合适的子控件,才会本身处理down事件。由于,消费了down事件,意味着接下来该触控点的事件序列事件都会交给该view消费,若是viewGroup拦截了事件,那么子view就没法接收到任何事件消息。
viewGroup寻找子控件的步骤也不复杂。首先viewGroup会为他的子控件构造一个控件列表,构造的顺序是view的绘制顺序的逆序,也就是一个view的z轴系数越高,显示高度越高,在列表的顺序就会越靠前。这其实比较好理解,显示越高的控件确定是优先接收点击的。除了默认状况,咱们也能够进行自定义列表顺序,这里就不展开了。
viewGroup会按顺序遍历整个列表,判断触控点的位置是否在该view的范围内、该view是否能够点击等,寻找合适的子view。若是找到合适的子view,则会把down事件分发给他,若是该view接收事件,则会为他建立一个TouchTarget,将该触控id和view进行绑定,以后该触控点的事件就能够直接分发给他了。
而若是没有一个控件适合,那么会默认选取TouchTarget链表的最新一个节点。也就是当咱们多点触控时,两次手指按下,若是没有找到合适的子view,那么就被认为是和上一个手指点击的是同个view。所以,若是viewGroup当前有正在消费事件的子控件,那么viewGroup本身是不会消费down事件的。
接下来咱们看看源码分析(代码有点长,须要慢慢分析理解):
ViewGroup.java api29 public boolean dispatchTouchEvent(MotionEvent ev) { ... // 对遮盖状态进行过滤 if (onFilterTouchEventForSecurity(ev)) { // action的高9-16位表示索引值 // 低1-8位表示事件类型 // 只有down或者up事件才有索引值 final int action = ev.getAction(); // 获取到真正的事件类型 final int actionMasked = action & MotionEvent.ACTION_MASK; ... // 拦截内容的逻辑 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { ... } ... // 三个变量: // split表示是否须要对事件进行分裂,对应多点触摸事件 // newTouchTarget 若是是down或pointer_down事件的新的绑定target // alreadyDispatchedToNewTouchTarget 表示事件是否已经分发给targetview了 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; // 若是没有取消和拦截进入分发 if (!canceled && !intercepted) { ... // down或pointer_down事件,表示新的手指按下了,须要寻找接收事件的view if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 多点触控会有不一样的索引,获取索引号 // 该索引位于MotionEvent中的一个数组,索引值就是数组下标值 // 只有up或down事件才会携带索引值 final int actionIndex = ev.getActionIndex(); // 这个整型变量记录了TouchTarget中view所对应的触控点id // 触控点id的范围是0-31,整型变量中哪个二进制位为1,则对应绑定该id的触控点 // 例如 00000000 00000000 00000000 10001000 // 则表示绑定了id为3和id为7的两个触控点 // 这里根据是否须要分离,对触控点id进行记录, // 而若是不须要分离,则默认接收全部触控点的事件 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // down事件表示该触控点事件序列是一个新的序列 // 清除以前绑定到到该触控id的TouchTarget removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; // 若是子控件数目不为0并且还没绑定到新的id if (newTouchTarget == null && childrenCount != 0) { // 使用触控点索引获取触控点位置 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 从前到后建立view列表 final ArrayList<View> preorderedList = buildTouchDispatchChildList(); // 判断是不是自定义view顺序 final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); 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是否能够接受触摸事件和是否在点击的范围内 if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // 检查该子view是否在touchTarget链表中 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // 链表中已经存在该子view,说明这是一个多点触摸事件 // 即两次都触摸到同一个view上 // 将新的触控点id绑定到该TouchTarget上 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // 找到合适的子view,把事件分发给他,看该子view是否消费了down事件 // 若是消费了,须要生成新的TouchTarget // 若是没有消费,说明子view不接受该down事件,继续循环寻找合适的子控件 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 保存该触控事件的相关信息 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(); // 保存该view到target链表 newTouchTarget = addTouchTarget(child, idBitsToAssign); // 标记已经分发给子view,退出循环 alreadyDispatchedToNewTouchTarget = true; break; } ... }// 这里对应for (int i = childrenCount - 1; i >= 0; i--) ... }// 这里对应判断:(newTouchTarget == null && childrenCount != 0) if (newTouchTarget == null && mFirstTouchTarget != null) { // 没有子view接收down事件,直接选择链表尾的view做为target newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } }// 这里对应if (actionMasked == MotionEvent.ACTION_DOWN...) }// 这里对应if (!canceled && !intercepted) ... }// 这里对应if (onFilterTouchEventForSecurity(ev)) ... }
通过了拦截与寻找消费down事件的控件以后,不管前面的处理结果如何,最终都是须要将事件进行派发,无论是派发给本身仍是子控件。这里派发的对象只有两个:viewGroup自身或TouchTarget。
通过了前面的寻找消费down事件子控件步骤,那么每一个触控点都找到了消费本身事件序列的控件并绑定在了TouchTarget中;而若是没有找到合适的子控件,那么消费的对象就是viewGroup本身。所以派发事件的主要任务就是:把不一样触控点的信息分发给合适的viewGroup或touchTarget。
派发的逻辑须要结合前面MotionEvent和TouchTarget的内容。咱们知道MotionEvent包含了当前屏幕全部触控点信息,而viewGroup的每一个TouchTarget则包含了不一样的view所感兴趣的触控点。
若是不须要进行事件分离,那么直接将当前的全部触控点的信息都发送给每一个TouchTarget便可;
若是须要进行事件分离,那么会将MotionEvent中不一样触控点的信息拆开分别建立新的MotionEvent,并发送给感兴趣的子控件;
若是TouchTarget链表为空,那么直接分发给viewGroup本身;因此touchTarget不为空的状况下,viewGroup本身是不会消费事件的,这也就意味着viewGroup和其中的view不会同时消费事件。
上图展现了须要事件分离的状况下进行的事件分发。
在把原MotionEvent拆分红多个MotionEvent时,不只须要把不一样的触控点信息进行分离,还须要对坐标进行转换和改变事件类型:
viewGroup中真正执行事件派发的关键方法是 dispatchTransformedTouchEvent
,该方法会完成关键的事件分发逻辑。源码分析以下:
ViewGroup.java api29 // 该方法接收原MotionEvent事件、是否进行取消、目标子view、以及目标子view感兴趣的触控id // 若是不是取消事件这个方法会把原MotionEvent中的触控点信息拆分出目标view感兴趣的触控点信息 // 若是是取消事件则不须要拆分直接发送取消事件便可 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; } // oldPointerIdBits表示如今全部的触控id // desirePointerIdBits来自于该view所在的touchTarget,表示该view感兴趣的触控点id // 由于desirePointerIdBits有可能全是1,因此须要和oldPointerIdBits进行位与 // 获得真正可接收的触控点信息 final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // 控件处于不一致的状态。正在接受事件序列却没有一个触控点id符合 if (newPointerIdBits == 0) { return false; } // 来自原始MotionEvent的新的MotionEvent,只包含目标感兴趣的触控点 // 最终派发的是这个MotionEvent final MotionEvent transformedEvent; // 二者相等,表示该view接受全部的触控点的事件 // 这个时候transformedEvent至关于原始MotionEvent的复制 if (newPointerIdBits == oldPointerIdBits) { // 当目标控件不存在经过setScaleX()等方法进行的变换时, // 为了效率会将原始事件简单地进行控件位置与滚动量变换以后 // 发送给目标的dispatchTouchEvent()方法并返回。 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; } // 复制原始MotionEvent transformedEvent = MotionEvent.obtain(event); } else { // 若是二者不等,说明须要对事件进行拆分 // 只生成目标感兴趣的触控点的信息 // 这里分离事件包括了修改事件的类型、触控点索引等 transformedEvent = event.split(newPointerIdBits); } // 对MotionEvent的坐标系,转换为目标控件的坐标系并进行分发 if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { // 计算滚动量偏移 final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); // 存在scale等变换,须要进行矩阵转换 if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } // 调用子view的方法进行分发 handled = child.dispatchTouchEvent(transformedEvent); } // 分发完毕,回收MotionEvent transformedEvent.recycle(); return handled; }
好了,了解完上面的内容,来看看viewGroup的 dispatchTouchEvent
中派发事件的代码部分:
ViewGroup.java api29 public boolean dispatchTouchEvent(MotionEvent ev) { ... // 对遮盖状态进行过滤 if (onFilterTouchEventForSecurity(ev)) { ... if (mFirstTouchTarget == null) { // 通过了前面的处理,到这里touchTarget依旧为null,说明没有找处处理down事件的子控件 // 或者down事件被viewGroup自己消费了,因此该事件由viewGroup本身处理 // 这里调用了dispatchTransformedTouchEvent方法来分发事件 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 已经有子view消费了down事件 TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; // 遍历全部的TouchTarget并把事件分发下去 while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 表示事件在前面已经处理了,不须要重复处理 handled = true; } else { // 正常分发事件或者分发取消事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 这里调用了dispatchTransformedTouchEvent方法来分发事件 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } // 若是发送了取消事件,则移除该target if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // 若是接收到取消获取up事件,说明事件序列结束 // 直接删除全部的TouchTarget 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); // 若是仅仅只是一个PONITER_UP // 清除对应触控点的触摸信息 removePointersFromTouchTargets(idBitsToRemove); } }// 这里对应if (onFilterTouchEventForSecurity(ev)) if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
到这里,viewGroup的事件分发源码就解析完成了,这里再来小结一下:
viewGroup中的源码不少,但大致的逻辑也就这三大部分。理解好MotionEvent和TouchTarget的设计,那么理解viewGroup的事件分发源码也是手到擒来。上面的源码我省略了一些细节内容,下面附上完整的viewGroup分发代码。
ViewGroup.java api29 public boolean dispatchTouchEvent(MotionEvent ev) { // 一致性检验器,用于调试用途 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // 辅助功能,用于辅助有障碍人群使用; // 若是这个事件是辅助功能事件,那么他会带有一个target view,要求事件必须分发给该view // 若是setTargetAccessibilityFocus(false),表示取消辅助功能事件,按照常规的事件分发进行 // 这里表示若是当前是目标target view,则取消标志,直接按照普通分发便可 // 后面还有不少相似的代码,都是一样的道理 if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; // 对遮盖状态进行过滤 if (onFilterTouchEventForSecurity(ev)) { // action的高9-16位表示索引值 // 低1-8位表示事件类型 // 只有down或者up事件才有索引值 final int action = ev.getAction(); // 获取到真正的事件类型 final int actionMasked = action & MotionEvent.ACTION_MASK; // ACTION_DOWN事件,表示这是一个全新的事件序列,会清除全部的touchTarget,重置全部状态 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 判断是否须要拦截 final boolean intercepted; // down事件或者有target的非down事件则须要判断是否须要拦截 // 不然直接拦截本身处理 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 此标志为子view经过requestDisallowInterupt方法设置 // 禁止viewGroup拦截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 调用onInterceptTouchEvent判断是否须要拦截 intercepted = onInterceptTouchEvent(ev); // 恢复事件状态 ev.setAction(action); } else { intercepted = false; } } else { // 本身消费了down事件 intercepted = true; } // 若是已经被拦截、或者已经有了目标view,取消辅助功能的target标志 if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // 判断是否须要取消 // 这里有不少种状况须要发送取消事件 // 最多见的是viewGroup拦截了子view的ACTION_MOVE事件,致使事件序列中断 // 那么须要发送cancel事件告知该view,让该view作一些状态恢复工做 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 三个变量: // 是否须要对事件进行分裂,对应多点触摸事件 // newTouchTarget 若是是down或pointer_down事件的新的绑定target // alreadyDispatchedToNewTouchTarget 是否已经分发给target view了 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; // 下面部分的代码是寻找消费down事件的子控件 // 若是没有取消和拦截进入分发 if (!canceled && !intercepted) { // 若是是辅助功能事件,咱们会寻找他的target view来接收这个事件 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; // down或pointer_down事件,表示新的手指按下了,须要寻找接收事件的view if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 多点触控会有不一样的索引,获取索引号 // 该索引位于MotionEvent中的一个数组,索引值就是数组下标值 // 只有up或down事件才会携带索引值 final int actionIndex = ev.getActionIndex(); // 这个整型变量记录了TouchTarget中view所对应的触控点id // 触控点id的范围是0-31,整型变量中哪个二进制位为1,则对应绑定该id的触控点 // 例如 00000000 00000000 00000000 10001000 // 则表示绑定了id为3和id为7的两个触控点 // 这里根据是否须要分离,对触控点id进行记录, // 而若是不须要分离,则默认接收全部触控点的事件 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 清除以前获取到该触控id的TouchTarget removePointersFromTouchTargets(idBitsToAssign); // 若是子控件的数量等于0,那么不须要进行遍历只能给viewGroup本身处理 final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { // 使用触控点索引获取触控点位置 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 从前到后建立view列表 final ArrayList<View> preorderedList = buildTouchDispatchChildList(); // 这一句判断是不是自定义view顺序 final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; // 遍历全部子控件 for (int i = childrenCount - 1; i >= 0; i--) { // 得到真正的索引和子view final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // 若是是辅助功能事件,则优先给对应的target先处理 // 若是该view不处理,再交给其余的view处理 if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } // 检查该子view是否能够接受触摸事件和是否在点击的范围内 if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // 检查该子view是否在touchTarget链表中 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // 链表中已经存在该子view,说明这是一个多点触摸事件 // 将新的触控点id绑定到该TouchTarget上 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } // 设置取消标志 // 下一次再次调用这个方法就会返回true resetCancelNextUpFlag(child); // 找到合适的子view,把事件分发给他,看该子view是否消费了down事件 // 若是消费了,须要生成新的TouchTarget // 若是没有消费,说明子view不接受该down事件,继续循环寻找合适的子控件 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 保存信息 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(); // 保存该view到target链表 newTouchTarget = addTouchTarget(child, idBitsToAssign); // 标记已经分发给子view,退出循环 alreadyDispatchedToNewTouchTarget = true; break; } // 辅助功能事件对应的targetView没有消费该事件,则继续分发给普通view ev.setTargetAccessibilityFocus(false); }// 这里对应for (int i = childrenCount - 1; i >= 0; i--) if (preorderedList != null) preorderedList.clear(); }// 这里对应判断:(newTouchTarget == null && childrenCount != 0) if (newTouchTarget == null && mFirstTouchTarget != null) { // 没有子view接收down事件,直接选择链表尾的view做为target newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } }// 这里对应if (actionMasked == MotionEvent.ACTION_DOWN...) }// 这里对应if (!canceled && !intercepted) if (mFirstTouchTarget == null) { // 通过了前面的处理,到这里touchTarget依旧为null,说明没有找处处理down事件的子控件 // 或者down事件被viewGroup自己消费了,因此该事件由viewGroup本身处理 // 这里调用了dispatchTransformedTouchEvent方法来分发事件 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 已经有子view消费了down事件 TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; // 遍历全部的TouchTarget并把事件分发下去 while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 表示事件在前面已经处理了,不须要重复处理 handled = true; } else { // 正常分发事件或者分发取消事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 这里调用了dispatchTransformedTouchEvent方法来分发事件 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } // 若是发送了取消事件,则移除该target if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // 若是接收到取消获取up事件,说明事件序列结束 // 直接删除全部的TouchTarget 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); // 若是仅仅只是一个PONITER_UP // 清除对应触控点的触摸信息 removePointersFromTouchTargets(idBitsToRemove); } }// 这里对应if (onFilterTouchEventForSecurity(ev)) if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
无论是viewGroup本身处理事件,仍是view处理事件,若是没有被子类拦截(子类重写方法),最终都会调用到 view.dispatchTouchEvent
方法来处理事件。view处理事件的逻辑就比viewGroup简单多了,由于它不须要向下去分发事件,只须要本身处理。总体的逻辑以下:
咱们先看到 view.dispatchTouchEvent
方法源码:
View.java api29 public boolean dispatchTouchEvent(MotionEvent event) { // 首先处理辅助功能事件 if (event.isTargetAccessibilityFocus()) { // 本控件没有获取到焦点,不处理事件 if (!isAccessibilityFocusedViewOrHost()) { return false; } // 获取到焦点,按照常规处理事件 event.setTargetAccessibilityFocus(false); } // 表示是否消费事件 boolean result = false; // 一致性检验器,检验事件是否一致 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } // 若是是down事件,中止嵌套滑动 final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { stopNestedScroll(); } // 安全过滤,本窗口位于非全屏窗口之下时,可能会阻止控件处理触摸事件 if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { // 若是事件为鼠标拖动滚动条 result = true; } // 先调用onTouchListener监听器 // 当咱们设置onTouchEventListener以后,L ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 若onTouchListener没有消费事件,调用onTouchEvent方法 if (!result && onTouchEvent(event)) { result = true; } } // 一致性检验 if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // 若是是事件序列终止事件或者没有消费down事件,终止嵌套滑动 if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
源码内容不长,主要的逻辑内容上面已经讲了,其余的都是一些细节的处理。onTouchListener通常状况下咱们是不会使用,那么接下来咱们直接看到onTouchEvent方法。
onTouchEvent整体上就作一件事:根据按下状况选择触发onClickListener或者onLongClickListener ,也就是判断是单击仍是长按事件,其余的源码都是实现细节。onTouchEvent方法正确处理每个事件类型,来确保点击与长按监听器能够被准确地执行。理解onTouchEvent的源码以前,有几个重要的点须要先了解一下。
咱们的操做模式有按键模式、触摸模式。按键模式对应的是外接键盘或者之前的老式键盘机,在按键模式下咱们要点击一个按钮一般都是先使用方向光标选中一个button(也就是让该button获取到focus),而后再点击确认按下一个button。可是在触摸模式下,button却不须要获取焦点。若是一个view在触摸模式下能够获取焦点,那么他将没法响应点击事件,也就是没法调用onClickListener监听器 ,例如EditText。
view辨别单击和长按的方法是设置延时任务,在源码中会看到不少的相似的代码,这里延时任务使用handler来实现。当一个down事件来临时,会添加一个延时任务到消息队列中。若是时间到尚未接收到up事件,说明这是个长按事件,那么就会调用onLongClickListener监听器,而若是在延时时间内收到了up事件,那么说明这是个单击事件,取消这个延时的任务,并调用onClickListener。判断是不是一个长按事件,调用的是 checkForLongClick
方法来设置延时任务:
// 接收四个参数: // delay:延时的时长;x、y: 触控点的位置;classification:长按类型分类 private void checkForLongClick(long delay, float x, float y, int classification) { // 只有是能够长按或者长按会显示工具提示的view才会建立延时任务 if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) { // 标志还没触发长按 // 若是延迟时间到,触发长按监听,这个变量 就会被设置为true // 那么当up事件到来时,就不会触摸单击监听,也就是onClickListener mHasPerformedLongPress = false; // 建立CheckForLongPress // 这是一个实现Runnable接口的类,run方法中回调了onLongClickListener if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } // 设置参数 mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); mPendingCheckForLongPress.setClassification(classification); // 使用handler发送延时任务 postDelayed(mPendingCheckForLongPress, delay); } }
上面这个方法的逻辑仍是比较简单的,下面看看 CheckForLongPress
这个类:
private final class CheckForLongPress implements Runnable { ... @Override public void run() { if ((mOriginalPressedState == isPressed()) && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { recordGestureClassification(mClassification); // 在延时时间到以后,就会运行这个任务 // 调用onLongClickListener监听器 // 并设置mHasPerformedLongPress为true if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } } ... }
延迟时间结束后,就会运行 CheckForLongPress
对象,回调onLongClickListener,这样就表示这是一个长按的事件了。
另外,在默认的状况下,当咱们按住一个view,而后手指滑动到该view所在的范围以外,那么系统会认为你对这个view已经不感兴趣,因此没法触发单击和长按事件。固然,不少时候并非如此,这就须要具体的view来重写onTouchEvent逻辑了,可是view的默认实现是这样的逻辑。
好了,那么接下来就来看一下完整的 view.onTouchEvent
代码:
View.java api29 public boolean onTouchEvent(MotionEvent event) { // 获取触控点坐标 // 这里咱们发现他是没有传入触控点索引的 // 因此默认状况下view是只处理索引为0的触控点 final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); // 判断是不是可点击的 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 一个被禁用的view若是被设置为clickable,那么他仍旧是能够消费事件的 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { // 若是是按下状态,取消按下状态 setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // 返回是否能够消费事件 return clickable; } // 若是设置了触摸事件代理你,那么直接调用代理来处理事件 // 若是代理消费了事件则返回true if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 若是该控件是可点击的,或者长按会出现工具提示 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // 若是是长按显示工具类标志,回调该方法 if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } // 若是是不可点击的view,同时会清除全部的标志,恢复状态 if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } // 判断是不是按下状态 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 若是能够获取焦点可是没有得到焦点,请求获取焦点 // 正常的触摸模式下是不须要获取焦点,例如咱们的button // 可是若是在按键模式下,须要先移动光标选中按钮,也就是获取focus // 再点击确认触摸按钮事件 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // 确保用户看到按下状态 setPressed(true, x, y); } // 两个参数分别是:长按事件是否已经响应、是否忽略本次up事件 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 这是一个单击事件,还没到达长按的时间,移除长按标志 removeLongPressCallback(); // 只有不能获取焦点的控件才能触摸click监听 if (!focusTaken) { // 这里使用发送到消息队列的方式而不是当即执行onClickListener // 缘由在于能够在点击前触发一些其余视觉效果 if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClickInternal(); } } } // 取消按下状态 // 这里也是个post任务 if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // 若是发送到队列失败,则直接取消 mUnsetPressedState.run(); } // 移除单击标志 removeTapCallback(); } // 忽略下次up事件标志设置为false mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: // 输入设备源是不是可触摸屏幕 if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } // 标志是不是长按 mHasPerformedLongPress = false; // 若是是不可点击的view,说明是长按提示工具的view // 直接检查是否发生了长按 if (!clickable) { // 这个方法会发送一个延迟的任务 // 若是延迟时间到仍是按下状态,那么就会回调onLongClickListener接口 checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); break; } // 判断是不是鼠标右键或者手写笔的第一个按钮 // 特殊处理直接返回 if (performButtonActionOnTouchDown(event)) { break; } // 向上遍历view查看是否在一个可滑动的容器中 boolean isInScrollingContainer = isInScrollingContainer(); // 若是在一个可滑动的容器中,那么须要延迟一小会再响应反馈 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); // 利用消息队列来延迟检测一个单击事件,延迟时间是ViewConfiguration.getTapTimeout() // 这个时间是100ms postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // 没有在可滑动的容器中,直接响应触摸反馈 // 设置按下状态为true setPressed(true, x, y); checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } break; case MotionEvent.ACTION_CANCEL: // 取消事件,恢复全部的状态 if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: // 通知view和drawable热点改变 // 暂时不知道什么意思 if (clickable) { drawableHotspotChanged(x, y); } final int motionClassification = event.getClassification(); final boolean ambiguousGesture = motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; int touchSlop = mTouchSlop; // view已经被设置了长按标志且目前的事件标志是模糊标志 // 系统并不知道用户的意图,因此即便滑出了view的范围,并不会取消长按标志 // 而是延长越界的偏差范围和检查长按的时间 // 由于这个时候系统并不知道你是想要长按仍是要滑动,结果就是两种行为都没有响应 // 由你接下来的行为决定 if (ambiguousGesture && hasPendingLongPressCallback()) { final float ambiguousMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier(); // 判断此时触控点的位置是否还在view的范围内 // touchSlop是一个小范围的偏差,超出view位置slop距离依旧断定为在view范围内 if (!pointInView(x, y, touchSlop)) { // 移除原来的长按标志 removeLongPressCallback(); // 延长等待时间,这里是原来长按等待的两倍 long delay = (long) (ViewConfiguration.getLongPressTimeout() * ambiguousMultiplier); // 减去已经等待的时间 delay -= event.getEventTime() - event.getDownTime(); // 添加新的长按标志 checkForLongClick( delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } touchSlop *= ambiguousMultiplier; } // 判断此时触控点的位置是否还在view的范围内 // touchSlop是一个小范围的偏差,超出view位置slop距离依旧断定为在view范围内 if (!pointInView(x, y, touchSlop)) { // 若是已经超出范围,直接移除点击标志和长按标志,点击和长按事件均没法响应 removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // 取消按下标志 setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; // 表示用户在屏幕上用力按压,加快长按响应速度 if (deepPress && hasPendingLongPressCallback()) { // 移除原来的长按标志,直接响应长按事件 removeLongPressCallback(); checkForLongClick( 0 /* 延迟时间为0 */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); } break; } return true; } // 对应if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) return false; }
若是你能看到这里,说明你对于viewGroup和view的事件处理源码已经了如指掌了。(高兴之余不如给笔者点个赞?(: ~)
最后这里再来总结一下:
dispatchTouchEvent
方法来分发事件,那么这里就会分为两种状况:
dispatchTouchEvent
主要内容是处理事件。首先会调用onTouchListener,若是其没有处理则会调用onTouchEvent方法。
到此本文的内容就结束了,事件分发的总体流程回顾、学了事件分发有什么做用、高频面试题相关文章,将会在后续继续创做。
在学习过程当中,如下相关资料给了我很是大的帮助,都是很是优秀的文章:
全文到此,原创不易,以为有帮助能够点赞收藏评论转发。
笔者才疏学浅,有任何想法欢迎评论区交流指正。
如需转载请评论区或私信交流。另外欢迎光临笔者的我的博客:传送门