前言
很快乐遇见你~
在上一篇文章 Android事件散发机制一:事件是如何达到activity的? 中,咱们探讨了触摸信息从屏幕产生到发送给具体 的view解决的整体流程,这里先来简略回顾一下:
- 触摸信息从手机触摸屏幕时产生,通过IMS和WMS发送到viewRootImpl
- viewRootImpl把触摸信息传递给他所治理的view
- view依据本身的逻辑对事件进行散发
- 常见的如Activity布局的顶层viewGroup为DecorView,他对事件散发办法进行了从新,会优先回调windowCallBack也就是Activity的散发办法
- 最初事件都会交给viewGroup去分发给子view
后面的散发步骤咱们分明了,那么viewGroup是如何对触摸事件进行散发的呢?View又是如何解决触摸信息的呢?这是整个事件散发的外围逻辑,也正是本文要探讨的内容。
事件处理中波及到的要害办法就是 dispatchTouchEvent
,不论是viewGroup还是view。在viewGroup中,dispatchTouchEvent
办法次要是把事件分发给子view,而在view中,dispatchTouchEvent
次要是解决生产事件。而次要的生产事件内容是在 onTouchEvent
办法中。上面探讨的是viewGroup与view的默认实现,而在自定义view中,通常会重写 dispatchTouchEvent
和 onTouchEvent
办法,例如DecorView等。
秉着逻辑后行源码后到的准则,本文尽管波及到大量的源码,但会优先讲清楚流程,有工夫的读者依然倡议浏览残缺源码。
了解MotionEvent
事件散发中波及到一个很重要的点:多点触控,这是在很多的文章中没有体现进去的。而要了解viewGroup如何解决多点触控,首先须要对触摸事件信息类:MotionEvent,有肯定的意识。MotionEvent中承载了触摸事件的很多信息,了解它更有利于咱们了解viewGroup的散发逻辑。所以,首先须要先了解MotionEvent。
触摸事件的根本类型有三种:
- ACTION_DOWN: 示意手指按下屏幕
- ACTION_MOVE: 手指在屏幕上滑动时,会产生一系列的MOVE事件
- ACTION_UP: 手指抬起,来到屏幕
一个残缺的触摸事件系列是:从ACTION_DOWN开始,到ACTION_UP完结 。这其实很好了解,就是手指按下开始,手指抬起完结。
手指可能会在屏幕上滑动,那么两头会有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE…、ACTION_UP。
这是失常的状况,而如果呈现了一些异样的状况,事件序列被中断,那么会产生一个勾销事件:
- ACTION_CANCEL:当出现异常状况事件序列被中断,会产生该类型事件
所以,残缺的事件序列是:从ACTION_DOWN开始,到ACTION_UP或者ACTION_CANCEL完结 。当然,这是咱们一个手指的状况,那么在多指操作的状况是怎么样的呢?这里须要引入另外的事件类型:
- ACTION_POINTER_DOWN: 当曾经有一个手指按下的状况下,另一个手指按下会产生该事件
- ACTION_POINTER_UP: 多个手指同时按下的状况下,抬起其中一个手指会产生该事件
区别于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是如何散发事件的。
ViewGroup对于事件的散发
这一步能够说是事件散发中的重头戏了。不过在了解了下面的MotionEvent之后,对于ViewGroup的散发细节也就容易了解了。
整体来说,ViewGroup散发事件分为三个大部分,前面的内容也会围绕着三大部分开展:
- 拦挡事件:在肯定状况下,viewGroup有权力抉择拦挡事件或者交给子view解决
- 寻找接管事件序列的控件:每一个须要分发给子view的down事件都会先寻找是否有适宜的子view,让子view来生产整个事件序列
- 派发事件:把事件散发到感兴趣的子view中或本人解决
大体的流程是:每一个事件viewGroup会先判断是否要拦挡,如果是down事件(这里的down事件示意ACTION_DOWN和ACTION_POINTER_DOWN,下同),还须要挨个遍历子view看看是否有子view生产了down事件,最初再把事件派发上来。
在开始解析之前,必须先理解一个要害对象:TouchTarget。
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遮挡住的时候,那么有可能被恶意软件操作产生危险。例如咱们看到的界面是这样的:
但实际上,咱们看到的这个按钮时不可点击的,实际上触摸事件会被散发到这个按钮前面的真正接管事件的按钮:
而后咱们就白给了。这个平安拦挡行为由两个标记管制:
- FILTER_TOUCHES_WHEN_OBSCURED:这个标记能够手动给控件设置,示意被非全屏控件笼罩时,间接过滤掉所有触摸事件。
- FLAG_WINDOW_IS_OBSCURED:这个标记示意以后窗口被一个非全屏控件笼罩。
具体的源码如下:
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()
办法。
- 子view能够通过requestDisallowInterupt办法强制要求viewGroup不要拦挡事件,viewGroup中会设置一个FLAG_DISALLOW_INTERCEPT标记示意不拦挡事件。然而以后事件序列完结后,这个标记会被革除。如果需要的话须要再次调用requestDisallowInterupt办法进行设置。
- 如果子view没有强制要求不拦挡,那么会调用
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事件的子控件
对于每一个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时,不仅须要把不同的触控点信息进行拆散,还须要对坐标进行转换和扭转事件类型:
- 咱们接管到的触控点的地位信息并不是基于屏幕坐标系,而是基于以后view的坐标系。所以当viewGroup往子view散发事件时,须要把触控点的信息转换成对应view的坐标系。
-
viewGroup收到的事件类型和子view收到的事件类型并不是完全一致的,在分发给子view的时候,viewGroup须要对事件类型进行批改,个别有以下状况须要批改:
- viewGroup收到一个ACTION_POINTER_DOWN事件分发给一个子view,然而该子view后面没有收到其余的down事件,所以对于该view来说这是一个簇新的事件序列,所以须要把这个ACTION_POINTER_DOWN事件类型改为ACTION_DOWN再发送给子view。
- viewGroup收到一个ACTION_POINTER_DOWN或ACTION_POINTER_UP事件,假如这个事件类型对应触控点2,然而有一个子view他只对触控点1的事件序列感兴趣,那么在拆散出触控点1的信息之后,还须要把事件类型改为ACTION_MOVE再分发给该子view。
- 留神,把原MotionEvent对象拆分为多个MotionEvent对象之后,触控点的索引也产生了扭转,如果须要散发一个ACTION_POINTER_DOWN/UP事件给子view,那么须要留神更新触控点的索引值。
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的事件散发源码就解析实现了,这里再来小结一下:
- 每一个触控点的事件序列,只能给一个view生产;如果一个view生产了一个触控点的down事件,那么该触控点的后续事件都会给他解决。
-
每一个事件达到viewGroup,如果须要散发到子view,那么viewGroup会新判断是否要拦挡。
- 当viewGroup的touchTarget!=null || 事件的类型为down 须要进行判断是否拦挡;
- 判断是否拦挡受两个因素影响:onInterceptTouchEvent和FLAG_DISALLOW_INTERCEPT标记
-
如果该事件是down类型,那么须要遍历所有的子控件判断是否有子控件生产该down事件
- 当有新的down事件被生产时,viewGroup会把该view和对应的触控点id绑定起来存储到touchTarget中
-
依据后面的解决状况,将事件派发到viewGroup本身或touchTarget中
- 如果touchTarget==null,阐明没有子控件生产了down事件,那么viewGroup本人处理事件
- 否则将事件拆散成多个MotionEvent,每个MotionEvent只蕴含对应view感兴趣的触控点的信息,并派发给对应的子view
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;
}
View对于事件的散发
不论是viewGroup本人处理事件,还是view处理事件,如果没有被子类拦挡(子类重写办法),最终都会调用到 view.dispatchTouchEvent
办法来处理事件。view处理事件的逻辑就比viewGroup简略多了,因为它不须要向上来散发事件,只须要本人解决。整体的逻辑如下:
- 首先判断是否被其余非全屏view笼罩。这和下面viewGroup的安全性查看是一样的
- 通过查看之后先查看是否有onTouchListener监听器,如果有则调用它
-
如果第2步没有生产事件,那么会调用onTouchEvent办法来处理事件
- 这个办法是view处理事件的外围,外面蕴含了点击、双击、长按等逻辑的解决须要重点关注。
咱们先看到 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的事件处理源码曾经一目了然了。(快乐之余不如给笔者点个赞?(: ~)
最初这里再来总结一下:
- 触摸事件,从屏幕产生后,通过零碎服务的解决,最终会发送到viewRootImpl来进行散发;
-
viewRootImpl会调用它所治理的view的
dispatchTouchEvent
办法来散发事件,那么这里就会分为两种状况:- 如果是view,那么会间接处理事件
- 如果是viewGroup,那么会向下派发事件
-
viewGroup会为每个触控点尽量寻找感兴趣的子view,最初再本人处理事件。viewGroup的工作就是把事件散发依照准则精准地分发给他子view。
- 事件散发中一个十分重要的准则就是:一个触控点的事件序列,只能给一个view生产,除了非凡状况,如被viewGroup拦挡。
- viewGroup为了践行这个准则,touchTarget的设计是十分重要的;他将view与触控点进行绑定,让一个触控点的事件只会给一个view生产
-
view的
dispatchTouchEvent
次要内容是处理事件。首先会调用onTouchListener,如果其没有解决则会调用onTouchEvent办法。- onTouchEvent的默认实现中的次要工作就是分别单击与长按事件,并回调onClickListener与onLongClickListener
到此本文的内容就完结了,事件散发的整体流程回顾、学了事件散发有什么作用、高频面试题相干文章,将会在后续持续创作。
原创不易,你的点赞是我最大的能源。感激浏览 ~
优良文献
在学习过程中,以下相干材料给了我十分大的帮忙,都是十分优良的文章:
- 《深刻了解android卷Ⅲ》:学习android零碎必备,作者对于android零碎的了解十分透彻,能够帮忙咱们意识到最实质的常识,而不是停留在表层。但对于老手可能会比拟难以读懂。
- 《Android开发艺术摸索》:进阶学习android必备,作者讲得比拟通俗易懂。深度可能相对而言可能较浅,但对老手比拟敌对,例如笔者。
- Android 触摸事件散发机制(三)View触摸事件散发机制 : 这篇文章采纳拆分源码的思路来解说源码,更好地排汇源码中的内容,笔者也是借鉴了他的写法来创作本文。文中对于源码的剖析十分到位,值得一看。
- 安卓自定义View进阶-事件散发机制详解 : 作者语言风趣,通俗易懂,不可多得的好文。
- Android事件散发机制 详解攻略,您值得领有 : 驰名博主carson_Ho的文章。特点是干货满满。全文无废话,只讲重要知识点,适宜用来温习知识点。
- Android事件散发机制 : gityuan大佬的博客,对于源码的钻研都很深刻。但对于一些源码细节并没有做过多的解释,有些中央难以了解。
全文到此,原创不易,感觉有帮忙能够点赞珍藏评论转发。
笔者满腹经纶,有任何想法欢送评论区交换斧正。
如需转载请评论区或私信交换。另外欢迎光临笔者的集体博客:传送门
发表回复