前言
很快乐遇见你~
在上一篇文章 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 大佬的博客,对于源码的钻研都很深刻。但对于一些源码细节并没有做过多的解释,有些中央难以了解。
全文到此,原创不易,感觉有帮忙能够点赞珍藏评论转发。
笔者满腹经纶,有任何想法欢送评论区交换斧正。
如需转载请评论区或私信交换。另外欢迎光临笔者的集体博客:传送门