概念
如果事件被处理了,返回 true,否则返回 false。有些文章说事件被消费了,其实意思和被处理了一样。
本文会首先讲解正常的处理流程,包括 Down、Move、Up、Cancel 等事件。然后会讲解如长按事件、多指事件等事件的分发和处理。看到这里,你会想,文章是不是很长?没错,正如你所料想的一样,这篇文章就像老太婆的裹脚布,又臭又长。其实就我个人经历而言,网上那么多的文章,其实都是大同小异。什么分发流程,值的返回流程,看了也解决不了问题,反而让人头晕,所以我才下定决心好好研究一番源码。看懂了源码,我才能真正的解决实际项目中遇到的问题。所以这篇文章就是基于源码角度的。但是别担心,涉及到分发流程的方法不会删减代码,并且会做到几乎每行代码都有注释的程度,所以别担心看不懂。
对于所有的事件分发中的调用,都有一个明确的逻辑返回关系(如下)。差异只是在得到返回值之后的后续处理过程。
对于得到返回值之后的处理方式,本文会分段讲解,最后进行汇总。
对于所有的事件分发,都是从最外层 Activity 向里面的 View 传递,在事件分发的过程中,其涉及到的层次结构如下:
各层次讲解:
而事件分发流程中,主要涉及三个 方法:
对于所有的事件分发,分发的起点和终点都是 Activity。Activity 中需要关注两个方法:dispatchTouchEvent、onTouchEvent。一个负责事件的分发,一个负责事件的处理。Activity.dispatchEvent 是事件分发流程的起点,其分发代码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
// 进入 Window
if (getWindow().superDispatchTouchEvent(ev)) {
// 事件被消费,返回 true
return true;
}
// 事件未被消费,调用自身的 onTouchEvent 方法,该方法默认实现永远返回 false
return onTouchEvent(ev);
}
其流程如下:
当子 View 都没有处理事件时,Activity 自身会调用 onTouchEvent 处理事件,不过 Activity 的 onTouchEvent 的默认实现永远返回 false。别问我从哪里知道的,方法注释里写的清清楚楚的。
在上面 Activity 的分发流程中,其会首先调用getWindow().superDispatchTouchEvent(ev)
方法,其中 getWindow 会返回当前 Activity 的 Window 对象,然后调用 Window 的 superDispatchTouchEvent 方法,但 Window 中的方法如下:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
可以看到,该方法是一个抽象方法,只是被定义了,并未被实现。具体的方法实现应该到实现类去寻找。前面我们讲过 Window 有一个唯一的 实现类 PhoneWindow (该知识点可以通过阅读 Window 的说明文档得到),那我们便看看 PhoneWindow 中的 superDispatchTouchEvent 方法是怎么实现的。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// mDecor 是当前 Window 持有的 DecorView 实例
return mDecor.superDispatchTouchEvent(event);
}
可以看到,PhoneWindow 继续调用 DecorView 的 superDispatchTouchEvent 方法。由此我们可以得出,事件能否被处理,是取决于 DecorView 中的处理方式的。让我们看看 DecorView 中的处理。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看出,DecorView 继续调用父类的 dispatchTouchEvent 方法,而 DecorView 是继承自 FrameLayout 的,那让我们去 FrameLayout 中找找看。很遗憾,FrameLayout 中是没有实现 dispatchTouchEvent 方法的。而在 DecorView 中点击跳转,最终会跳到 ViewGroup 的 dispatchTouchEvent 方法中,说明 FrameLayout 将事件的分发交给了父类 ViewGroup 处理。ViewGroup 可是事件分发的主角之一,让我们看看 ViewGroup 的 dispatchTouchEvent 方法。
首先我们先看看 disPatchEvent 的代码行数:
从 2541 到 2755,ViewGroup 的 dispatchTouchEvent 方法跨越了整整 214 行,是相当多了,那么便说明这个方法是非常复杂的。但是不担心,让我们来一步步梳理。
ViewGroup.dispatchTouchEvent 方法讲解开始
// 分发的主流程
public boolean dispatchTouchEvent(MotionEvent ev) {
// 方法开头的两个 if 判断,不重要,但此处还是讲讲作用
// 1、该变量在 View 中定义,用于调试。其作用是用于一致性确认,与主流程无关
// 在事件分发流程中,找不到的变量和方法可以到View和MotionEvent中寻找。因为索引不一定建立,在源码中才会标红
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// 2、该判断主要是为无障碍功能准备
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
// 事件是否被处理的标志
boolean handled = false;
// 当具备安全性时,事件可以进入主流程继续分发。安全性主要是指当前 Window 是否被隐藏或者遮挡
// 当窗口被遮挡,并且设置了被遮挡时事件不能向下传递。事件便会不被处理
if (onFilterTouchEventForSecurity(ev)) {
// 此处主流程代码省略,下面单独讲解
}
// 此代码用于调试,与主流程无关
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
上面的代码中需要注意的几个点:
看了总体流程,让我们来看看主流程:
if (onFilterTouchEventForSecurity(ev)) {
// 获取事件动作,是 Down、Move、Up、Cancel 等事件
final int action = ev.getAction();
// 获取 Action 表示的动作(此处有疑问?没事,代码下面会做解释)
// 简单的说,就是一个 Event 分为动作的信息和手指的信息,此处只取动作的信息
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 初始化按下事件。按下事件是一个事件序列的起始点。当系统监测到有按下事件时,都会去做相应的初始化操作
if (actionMasked == MotionEvent.ACTION_DOWN) {
// mFirstTouchTarget 是可处理触摸事件目标形成的链表的头指针,TouchTarget 可以使 View 与触摸事件形成映射关系
// 事件序列结束时,可能会出现因为一些特殊原因,如 ANR,导致 mFirstTouchTarget 指向的链表未被清空
// 此处在新的事件序列开始前清空链表,并重置点击状态
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 是否中断事件分发的标志
final boolean intercepted;
// mFirstTouchTarget 不为空表示,此时已经存在可以处理事件的目标 View
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 判断是否允许父 View 拦截事件,为 true 表示不允许父 View 拦截事件
// 详情可以阅读 ViewParent.requestDisallowInterceptTouchEvent(false) 的方法说明
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 父 View 可以拦截事件
if (!disallowIntercept) {
// 判断是否需要拦截事件,默认会对鼠标的左键按下拖动滚动条事件作拦截
intercepted = onInterceptTouchEvent(ev);
// 设置 Action,防止触摸动作因为特殊原因改变
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 不是按下事件,且事件序列链表头指针为空(不存在处理触摸事件的目标 View)。则拦截事件,不向下分发
intercepted = true;
}
// 无障碍相关,此处跳过
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 是否需要取消事件的标志
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 是否将一个MotionEvent拆分到多个子View的标志,
// 视图可能存在上下重叠的情况,此变量为 true 时表示重叠处的所有子 View 都可以处理触摸事件
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
// 新的可以处理触摸事件的目标对象。
TouchTarget newTouchTarget = null;
// 是否已经将事件分发到新的 TouchTarget 的标识
boolean alreadyDispatchedToNewTouchTarget = false;
// 事件不被拦截,并且不被取消
if (!canceled && !intercepted) {
// 无障碍时接收触摸事件的 View,此处不讲
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// 是按下事件或者多指按下事件。第三个事件不用关注,不在 onTouchEvent 的处理范围内,感兴趣的可以查看此变量的源码说明
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 对于按下事件,actionIndex 的值总为 0
final int actionIndex = ev.getActionIndex();
// 待分配的id位,final型,不可更改,可以理解将这个变量为一个 mask(不懂 mask,代码下面会讲)
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
// 清除之前的 touch targets(映射关系),重新获取
removePointersFromTouchTargets(idBitsToAssign);
// 拿到此 ViewGroup 的所有孩子数,准备遍历,找到目标 View
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();
// 是否是自定义顺序的标志,为 true 表示用户自定义了 View 的绘制顺序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 开始循环遍历,找出目标 View
for (int i = childrenCount - 1; i >= 0; i--) {
// 拿到子 View 在列表中的索引。如果没有自定义绘制顺序,索引的返回值就是默认传入的 i
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 拿到第 i 个位置的子 View,如果自定义顺序列表为空,就从默认的列表中获取,否则从自定义列表中获取
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 无障碍相关,此处不关注
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
// 设置为 null,下次不会走此处的 if 判断
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 如果子 View 不能接收事件,并且触摸事件没有在子 View 的范围内
// 则跳过本次执行,开始下一次执行
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 子 View 能够接收事件,并且触摸事件在子 View 的范围内
// 获取映射关系
newTouchTarget = getTouchTarget(child);
// 映射关系不为空,有能处理事件的child,中断循环
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 还没有形成映射关系,目前没有能处理事件的child
// 取消 View 暂时不能接收触摸事件的限制。CancelNextUpFlag 表示子 View 与 parent 暂时分离,不接收触摸事件
resetCancelNextUpFlag(child);
// 前面讲过,如果需要,目标 MotionEvent需要被分发到多个子 View 中,
// 但是目标 MotionEvent 并不一定满足能被分发到多个子 View 的条件,所以需要转换事件,再尝试分发
// 如果传入的子 View 为空,便会将事件交给父类(View)处理
// dispatchTransformedTouchEvent 第一处被调用的地方
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// mLastTouchDownTime 是个用于调试的变量,不用在意
// ----------不用在意的代码开始----------
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// ----------不用在意的代码结束----------
// 事件成功分发到子 View,建立映射关系
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 退出循环
break;
}
// 无障碍相关,不用关注
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
// 如果仍然没有找到合适的子 View,就从已有的映射关系中寻找一个最近的 TouchTarget 赋值
// 如果上面已经找到了合适的子 View,此处便不会执行
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
// 这里是链表的操作
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 非Down事件,直接进入这里,当有可以处理事件的对象时(处理Down事件时已经进行赋值),在这里寻找。
if (mFirstTouchTarget == null) {
// 没有可处理触摸事件的子 View,进行分发,交给父类处理(传入的 child 为空)
// dispatchTransformedTouchEvent 第二处被调用的地方
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
// 此处代码代表了一个重要的细节,就是如果Down事件被分发到了子View(mFirstTouchTarget != null),那么后续的非Down事件都会直接交给此View处理。不走分发流程
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 事件已经被分发处理
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
// 事件被处理,dispatchTransformedTouchEvent 第三处被调用的地方
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
// 链表操作,不关心
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 抬起事件,重置状态
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
// 多指触摸事件下的状态重置
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
ViewGroup.dispatchTouchEvent 方法讲解结束
是不是觉得上面的代码很多,很绕?没事,下面会对主流程进行更加细致的探讨。
// 一个 Action 通过与该掩码进行与运算,便可以得到对应的动作类型
// 0xff = 0x00ff,故动作类型是存储在低位的2个字节中的
public static final int ACTION_MASK = 0xff;
// 一个 Action 通过与该掩码进行与运算,便可得到对应的手指信息
// 由值可知手指信息是存储在高位的2个字节中的
public static final int ACTION_POINTER_INDEX_MASK = 0xff00;
// 暂时就讲这么多,更具体的会在后面的多指事件处理中讲解
让我们来看看 dispatchTransformedTouchEvent 都干了哪些事:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
// final 型,只能被赋值一次,代表着分发是个一次性操作
final boolean handled;
final int oldAction = event.getAction();
// 取消事件是个特殊的事件,本方法不会对该事件进行转换或者过滤。取消事件重要的不是事件的内容,而是事件本身的意义
// 取消事件会被分配到对应的View进行处理,Down 事件到来时不会走这个 if 判断,只有后面两个调用才会走这里。
// 当没有找到对应的 View 时(child==null),会将事件交给父类(View)处理,否则就交给对应的 View(child) 处理
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 计算需要传递的事件需要包含的手指的数量
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// 如果因为某些原因导致不正常的计算结果,比如一个触摸事件没有手指数量,那么此事件将被遗弃,不进行处理
if (newPointerIdBits == 0) {
return false;
}
/**
* 如果满足以下条件,我们可以使用原触摸事件(传入的触摸事件)。否则我们就应该对该事件进行拷贝后使用:
* 1、计算得到的新旧手指数量相同
* 2、我们不需要对原触摸事件进行额外的不可逆的转换
* 3、每一个对原触摸事件做出的更改,我们都进行还原
* */
// 进行 Down、Move、Up事件的分发处理,Cancel 已经在上面进行处理
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
// 进入此 if 语句体有两种情况
// 1、没有可以处理事件的 View(child==null)
// 2、有可以尝试进行分发的View(child!=null,注意此处与上面cancel事件描述的区别),并且View有单位矩阵(任何矩阵与单位矩阵相乘都等于其本身)
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 因为有 Move 事件,所以得进行偏移值处理
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// 分发给子 View
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
// 两种情况都不满足,获取拷贝
transformedEvent = MotionEvent.obtain(event);
} else {
// 对触摸事件进行拆分,以满足触摸事件中含有指定数量的手指的信息
transformedEvent = event.split(newPointerIdBits);
}
// 没有可处理事件的 View 时,都交给父类处理
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
// 没有单位矩阵,就使用逆矩阵转换事件(逆矩阵与原矩阵相乘等于单位矩阵)
transformedEvent.transform(child.getInverseMatrix());
}
// 尝试分发转换后的事件
handled = child.dispatchTouchEvent(transformedEvent);
}
// 回收时间,主要是回收代码中使用 C/C++ 写的部分,说明TouchEvent的实现相当底层
transformedEvent.recycle();
return handled;
}
梳理了上面的主流程,我们可以看出,当没有可以处理触摸事件的子View(child==null)时,触摸事件会交给父类View处理。否则就交给对应的子View处理。
支持,ViewGroup的分发也算是梳理的告一段落了。下面,让我们看看ViewGroup中的拦截方法,拦截方法很简单,默认的实现对大多数事件都不拦截:
/**
* 当事件源是鼠标,并且鼠标左键按下,在滚动条的按钮上时(这个条件不确定),才进行拦截(条件太苛刻)
* 故一般的事件,都不会拦截(姑且可以算是默认不拦截)
* */
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;
}
讲完了 onInterceptTouchEvent 方法,我们来讲讲 onTouchEvent 方法。但是很可惜,ViewGroup 中并没有定义 onTouchEvent 方法。onTouchEvent 方法是 View 中定义的,后面我们会讲到。现在先总结下ViewGroup中的分发过程。
经过一大段分析,画了个流程图出来,如果其中有不对的方法,欢迎大家指正。来总结一下:
上面将ViewGroup中的事件分发梳理了一次,还剩下一个View的分发流程未梳理。作为事件分发的双主角之二,让我们先来看看View.dispatchTouchEvent
方法,此方法会在ViewGroup.dispatchTransformedTouchEvent
方法中被调用到,他是事件分发在View中的起始部分:
public boolean dispatchTouchEvent(MotionEvent event) {
// 无障碍功能相关,此处不关注
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
// 事件是否被处理掉的标识
boolean result = false;
// 调试相关,此处不关注
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 是按下事件时,停止滑动
stopNestedScroll();
}
// 分发主流程
if (onFilterTouchEventForSecurity(event)) {
// 输入源是鼠标时,此处代码生效,一般情况下忽略它
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
// 事件最先由 onTouch 方法处理
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// onTouch 方法没有处理成功,由自己的 onTouchEvent 方法处理
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 调试相关
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// 非Move事件,停止滑动
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
View.dispatchTouchEvent
方法是相当简单的,它主要就做了两件事。
View.onTouchEvent
方法处理,该方法返回 true,表示事件被处理了,否则事件未被处理那让我们接着来看看View.onTouchEvent
方法,这个方法和ViewGroup.dispatchTouchEvent
方法可是事件分发中最重要的两个方法,并且它也是事件分发流程中的终点,所以这里就只关注主要的流程:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// View是否可点击的标识
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// 被设置为不可用的View仍然会消费事件,只是在触摸事件到来时不去响应
// 想要不可用的View不消费事件,还需要将View的clickable属性设置为false
return clickable;
}
// 如果View设置了触摸代理,则会消费事件,并将事件交给触摸代理处理。
// TouchDelegate的作用可以百度,这里就不详述。其主要是用于扩大View的实际可点击区域。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果View是可点击的,或者在长按情况下可以显示tooltip,事件就被消费,否则不会被消费
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
// Up事件
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// 处理 tooltip 相关事项
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
// 不可点击时设置状态,移除监听
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 处理按压的相关事项,比如View颜色的变化
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// 更改状态,保证用户能够感知到
setPressed(true, x, y);
}
// 此处 mHasPerformedLongPress 起作用了,如果长按事件没有执行,则会执行点击事件,否则不执行点击事件
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除长按事件监听
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
// 此对象用于执行点击方法,会调用会对象的OnClickListener,执行onClick方法。
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
// 与handler结合使用
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// 消息发送失败,立即进入非按压状态
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
// 按下事件
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
// 长按事件是否已经执行的标识
mHasPerformedLongPress = false;
// 不可点击
if (!clickable) {
// 长按事件相关
checkForLongClick(0, x, y);
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();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
// 检查长按事件,当长按事件条件满足时,会执行长按事件
checkForLongClick(0, x, y);
}
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:
if (clickable) {
drawableHotspotChanged(x, y);
}
if (!pointInView(x, y, mTouchSlop)) {
// 超出了视图的范围,就移除点击和长按事件的监听检查
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
由上面的代码,我们可以得出以下结论:
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
// 如果长按事件返回值为true,就不执行点击事件了
if (performLongClick(mX, mY)) {
// 该变量在点击事件执行时会用上,为true则点击事件不执行
mHasPerformedLongPress = true;
}
}
}
}
看完了分发流程,让我们来看看实际的问题解决。现在我有个实际布局如下:
说明:
View.OnClickListener
,绿色部分设置了View.OnClickListener
,红色部分设置了View.OnTouchListener
。言归正传,这个BUG要怎么改呢?首先,我们应该区分多选和非多选模式,这个好办,一个布尔型变量即可解决。其次,我们理清楚,在不同的模式下,谁响应点击事件。在多选模式下,应该是绿色部分响应点击事件。在非多选模式下,应该是红色部分、蓝色部分响应点击事件。
让我们继续捋捋,绿色部分是红色部分子布局,蓝色部分是绿色部分子布局,则蓝色部分是红色部分子布局。绿色部分要响应点击事件,事件就得从红色向下分发。蓝色部分要响应点击事件,事件就得从绿色向下分发。
现在我们得到了有用的信息。那让我们来看看未改正的有BUG的代码,绿色部分的布局代码(每个item的根布局),设置了View.OnClickListener
,View.OnClickListener
执行后会返回 true,此处就不列举代码了。同时重写了拦截方法 onInterceptTouchEvent:
public class CustomConstraintLayout extends ConstraintLayout {
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
// 多选模式拦截事件,不向下分发,非多选模式向下分发
// 即多选模式下,该变量为 true,非多选模式下为 false
return mIsIntercept;
}
}
而红色部分设置的代码如下:
rv.setOnTouchListener(new View.OntouchListener{
@Override
public boolean onTouch(View v, MotionEvent event) {
if(inputView.isVisible()) {
inputView.setGone(true);
}
return true;
}
});
而蓝色部分也设置了View.OnClickListener
。
这样做实现了效果:非多选模式下,蓝色部分可以响应点击事件,多选模式下,绿色部分可以响应点击事件。但是问题来了,非多选模式下&输入法布局可见时,点击RecyclerView中非蓝色部分时,输入法应该隐藏,但实际却不会隐藏。为什么呢?
非多选模式下,点击绿色部分,事件会一直分发到绿色部分,而绿色部分设置了OnClickListener,它会消费掉事件。返回值为true,根据上面讲过的分发流程,消费掉事件后,父类便不会再进行处理。所以,父类的OnTouchListener此时没有效果,点击屏幕输入法布局不会隐藏。那如何才能让父类的OnTouchListener生效呢?答案便是非多选模式下,绿色部分的OnClickListener不应该生效。那怎么才能做到这种效果呢?让我们想想上面将的onTouchEvent方法,onTouchEvent方法会调用OnClickListener和OnLongClickListener,消费掉事件,并返回true。这就是代表着事件被处理了。那么我们只要在非多选模式下,不处理事件(返回false),父类的OnTouch方法便可以执行了。所以重点是要重写 CustomConstraintLayout 的 onTouchEvent 方法,这也是官方在 onInterceptTouchEvent 的注释中特别强调的一点。话不多说,上代码:
public class CustomConstraintLayout extends ConstraintLayout {
@Override
public boolean onInTouchEvent(MotionEvent event) {
if(mIsIntercept) {
// 拦截模式下,复用父类的onTouchEvent,最终会调到自己的OnClickListener,返回结果即可
return super.onTouchEvent(event);
}
// 非拦截模式下,不执行
return false;
super.onInterceptTouchEvent(event);
// 多选模式拦截事件,不向下分发,非多选模式向下分发
// 即多选模式下,该变量为 true,非多选模式下为 false
return mIsIntercept;
}
}
经过这两步,BUG就算是修复了。让我们看看最终的代码:
public class CustomConstraintLayout extends ConstraintLayout {
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// 多选模式拦截事件,不向下分发,非多选模式向下分发
// 即多选模式下,该变量为 true,非多选模式下为 false
return mIsIntercept;
}
@Override
public boolean onInTouchEvent(MotionEvent event) {
if(mIsIntercept) {
// 拦截模式下,复用父类的onTouchEvent,最终会调到自己的OnClickListener,结果返回true,停止分发
// 调用 super.onTouchEvent(event) 并不是说走父布局分发的流程,而是复用父类(View)的代码,事件的实际处理者还是当前View
return super.onTouchEvent(event);
}
// 非拦截模式下,不处理事件,父View没有找到合适的对象,会调用自身的onTouchEvent,自己处理事件,这就可以响应隐藏输入法的布局的要求了
return false;
}
}
综上,要写出上面的代码,需要明白以下几件事:
讲讲三种方法的含义吧,如图。
那么事件分发的基础就暂时讲到这里吧。后面如果有时间,我会把多指的触摸事件给好好讲讲。这重点研究 MotionEvent 类和 ScrollView 类。
原文:https://www.cnblogs.com/wellcherish/p/11838691.html