1、基础认知
事件
在我们通过屏幕与手机交互的时候,每一次点击、长按、移动等都是一个个事件。按照面向对象的思想,这些一个个事件都被封装成了 MotionEvent 对象。
事件列
从手指解除屏幕到手指离开屏幕,这个过程产生了一系列的事件。一般情况下,事件列都是以 DOWN 事件开始,UP 事件结束,中间有无数的 MOVE 事件。这一个事件列中的所有事件,要么被忽略,要么就只能有一个事件能使用。要是同一个序列,比如从按下到移动这一系列的动作,不同的 View 都能接受的话,那整个界面就会非常混乱,而且逻辑很复杂。
事件分发的本质
所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的 View 并且进行处理,而这个传递的过程就是分发过程。
事件在哪些对象之间进行传递?
Activity、ViewGroup、View
事件分发的顺序
首先需要知道,Android 中的 View 是树状结构。每一个 Activity 内部都包含一个 Window 用来管理要显示的视图。而 Window 是一个抽象类,其具体实现是 PhoneWindow 类。DecorView 作为 PhoneWindow 的一个内部类,实际管理着具体视图的显示。DecorView 是FrameLayout 的子类,盛放着我们的标题栏和根视图。我们自己写的一些 View 和 ViewGroup 都是由他来管理的。事件分发的时候,顶层的这些“基础View”们实际上是不会对事件有任何操作的,他们只是把事件不断的向下递交,直到我们可以使用这些事件。
所以事件自顶向下的分发顺序是:
Activity(基本不处理) --> 根View --> 一层一层 ViewGroup(如果有的话) --> 子View
事件分发过程由哪些方法来协作完成?
dispatchTouchEvent : 分发事件
onInterceptTouchEvent : 拦截事件
onTouchEvent : 消费事件
1)三个方法的共同点:
他们具体是否执行了自己的功能(分发、拦截、消费)完全由自己的返回值来确定,返回 true 就表示自己完成了自己的功能(分发、拦截、消费)。
2)不同点:
dispatchTouchEvent() 和 onTouchEvent() 这两个方法,无论是 Activity、ViewGroup 还是 View,都会被用到。而 onInterceptTouchEvent() 方法因为只是为了拦截事件,那么 Activity 和 View 一个在最顶层,一个在最底层,也就没必要使用了。因此在 Activity 和 View 中是没有 onInterceptTouchEvent() 方法的。
接下来,会进入源码来分析整个事件分发机制。
2、Activity 事件分发源码分析
当一个点击事件发生时,事件最先传到 Activity 的 dispatchTouchEvent() 进行事件分发。
下面是 Activity 中的 dispatchTouchEvent 方法的源码。
// Activity.java
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
该方法被用来处理屏幕触摸事件。可以重写此方法来实现在事件在分发到 window 前拦截所有事件。当事件被消费时,该方法返回 true。
当为 ACTION_DOWN 事件时,调用 onUserInteraction() 方法,即一触摸屏幕就会触发此方法调用。onUserInteraction 在 Activity 中是一个空方法,可以通过重写该方法来实现一触摸便需要进行的逻辑,比如屏保功能。当当前 Activity 在栈顶时,触屏点击按 Home、Back、Menu 键等都会触发此方法。
当 getWindow().superDispatchTouchEvent(ev) 方法返回 true 时,整个方法返回 true,即该点击事件停止往下传递,事件传递过程结束。
getWindow(),获取 Window 类对象,Window 类是抽象类,其唯一实现类是 PhoneWindow,下面为 PhoneWindow 的 superDispatchTouchEvent 方法。
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看到,该方法调用了 DecorView 的 superDispatchTouchEvent 方法。
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
最终调用 ViewGroup 的 dispatchTouchEvent 方法,即 Activity 将事件传递给了 ViewGroup。
所以,getWindow().superDispatchTouchEvent(ev) 方法实现了将事件从 Activity 传递给 ViewGroup。
回到 Activity 的 dispatchTouchEvent 方法中,如果 getWindow().superDispatchTouchEvent(ev) 方法返回不为 true,即该事件未被 Activity 下任何一个 View 接收或处理时,则调用并返回 Activity 的 onTouchEvent(ev) 方法。
// Activity.java
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
如果 mWindow.shouldCloseOnTouch(this, event) 方法返回 ture,则 finish 掉 Activity 并返回 ture,其他情况均返回 false。看一下 Window 的 shouldCloseOnTouch 方法:
// Window.java
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
Window 的 shouldCloseOnTouch 方法主要是对于处理边界外点击事件的判断:是否是 ACTION_UP 事件,event 的坐标是否在边界内,或是否为 ACTION_OUTSIDE 事件等。
3、ViewGroup 事件分发源码分析
从 Activity 的事件分发源码可知,ViewGroup 事件分发过程从 dispatchTouchEvent() 开始。下面便是 ViewGroup 的 dispatchTouchEvent 的源码。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// handled 标志保存的是最终返回的结果,表示是否被消费,不继续分发,默认为否,继续分发。
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 判断是否为 ACTION_DOWN 事件,如果是的话,代表一个新的事件序列开始。
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
// 取消并清除所有触摸 target,将 mFirstTouchTarget 设置为 null
cancelAndClearTouchTargets(ev);
// 重置所有触摸状态,为新的事件序列做准备
resetTouchState();
}
// Check for interception.
// intercepted 标志保存的是是否进行拦截,下面就是做拦截检查
final boolean intercepted;
// 1、只有当前未 ACTION_DOWN 事件
// 2、当 ACTION_DOWN 事件被子 View 消费后处理其他事件时会调用该代码
// 因为此时 mFirstTouchTarget!=null(mFirstTouchTarget 为 TouchTarget 序列中的第一个对象)
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 可以调用方法 requestDisallowInterceptTouchEvent 来禁止 ViewGroup 的事件拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果没有禁止 ViewGroup 事件拦截
// 那么就会调用 ViewGroup 的 onInterceptTouchEvent 方法来确认是否进行拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// 如果禁止了 ViewGroup 事件拦截,那么 intercepted 标志直接设为 false
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
// 当不存在消耗 ACTION_DOWN 事件的目标控件时
// 后续事件的 intercepted 都会被设为 true
// 此时可以理解为 ViewGroup 退化成 View,事件处理将交给 super.dispatchTouchEvent() 进行
//
// 所以当我们当点击 ViewGroup 的空白位置时
// 由于不存在消耗 ACTION_DOWN 事件的子控件,导致 mFirstTouchTarget 为空
// 任何后续的事件到来时,intercepted 都会被设为 true 而被 ViewGroup 拦截
// 包括多点触控 ACTION_POINTER_DOWN 事件
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
// 检查该事件是否需要 cancel
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 事件分发
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 没有被取消或者当前 ViewGoup 不进行拦截
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// 这里开始对事件类型进行区分了,如果是 ACTION_DOWN,ACTION_POINTER_DOWN
// ACTION_DOWN 为第一个手指初次触摸到屏幕时触发
// ACTION_POINTER_DOWN 为有非主要的手指按下时触发,即按下之前已经有手指在屏幕上,即多点触控
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 触控点下标
final int actionIndex = ev.getActionIndex(); // always 0 for down
// 位分配 ID,通过触控点的 PointerId 计算
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
// 检查是否有记录的 PointID
// 假如 mFirstTouchTarget 不为空,检查 TouchTarget 序列,检索是否存在记录了该触控点的 TouchTarget
// 如果存在,则移除该触控点记录
// 移除后,如果 TouchTarget 不存在其他的触控点记录,则从序列中移除
removePointersFromTouchTargets(idBitsToAssign);
// 接下来开始遍历自己的子 View 们
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 获取到点击的坐标,用来从子 View 中筛选出点击到的 View
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
// 按从前往后的顺序开始遍历子 View
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 遍历子 View 判断哪个子 View 接受事件
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
// 筛选到不合适的子 View 时直接 continue
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 同样也是做筛选操作
if (!child.canReceivePointerEvents() // 判断控件是否可以接受事件,当控件可见性为 VISIBLE 或者正在执行动画时,返回 true
|| !isTransformedTouchPointInView(x, y, child, null)) { // 判断该子 View 是否包含事件的坐标
// 不可接受事件,或点击坐标不在其中,则跳过该子 View
ev.setTargetAccessibilityFocus(false);
continue;
}
// 当找到了合适的子 View 时,通过 getTouchTarget 方法获取 TouchTarget 序列中是否包含该子 View 的 touch target
// 未找到对应的 target 时返回空
newTouchTarget = getTouchTarget(child);
// 要是返回的结果不为空,即找到了可以向其分发事件的子 View,就跳出循环
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
// 如果 TouchTarget 序列中已经存在对应 View 的 TouchTarget
// 就直接把 idBitsToAssign 添加到 TouchTarget 中
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 就算没有在 TouchTarget 序列中找到该 View 的 TaouchTarget 结果也没关系
// 在这里继续调用 dispatchTransformedTouchEvent() 方法,并将该子 View 作为参数传递进去
// 该方法返回 true 表示事件分发给该子 View,此时将 alreadyDispatchedToNewTouchTarget 置为了 ture。
//
// 而代码块中的 addTouchTarget 方法中,
// 生成一个新的 TouchTarget(包裹着消化事件的 View)
// 并添加到了 TouchTarget 序列的头部,此时 mFirstTouchTarget 为这个新的 TouchTarget
// 返回的 newTouchTarget 此时和 mFirstTouchTarget 相同
// 且 newTouchTarget.next 为旧的 mFirstTouchTarget 值
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
// 经过前面的 for 循环没有找到消耗 ACTION_POINTER_DOWN 事件的 View
// 但是 mFirstTouchTarget 不为空
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
// 把 newTouchTarget 指向 TouchTarget 序列的最后的元素(一般即为消耗 ACTION_DOWN 事件的控件)
// 并把当次 ACTION_POINTER_DOWN 事件的 PointID 记录到该元素。
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 接下来就是对于其他事件的分发了
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 没有找到要接受事件的 View,或者被拦截了,这里调用了 dispatchTransformedTouchEvent() 并且传了一个 null 的 View 参数
// 即分配给 ViewGroup 自身
// 也就是说,如果子 View 没有消费事件,那么子 View 的上层 ViewGroup 会调用其 onTouchEvent() 处理Touch事件
// 此时 ViewGroup 像一个普通的 View 那样调用 dispatchTouchEvent() 方法,并且在 dispatchTouchEvent() 方法中调用 onTouchEvent() 方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
// 有 View 接受了 ACTION_DOWN 事件,那么这个 View 也将接受其余的事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// alreadyDispatchedToNewTouchTarget 变量在将 ACTION_DOWN,ACTION_POINTER_DOWN 事件传递给子 View 时设为了 true
// 同时 target 为这个子 View 的 target
// 所以这里直接标志 handled = true,避免重复分发事件
handled = true;
} else {
// 否则依然是递归调用 dispatchTransformedTouchEvent 方法
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
// 处理 ACTION_UP 和 ACTION_CANCEL
// 在此主要的操作是还原状态
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 这里说明这是最后一个触控点抬起,通过 resetTouchState 方法进行清理和还原状态。
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
// 如果是 ACTION_POINTER_UP 事件
// 那就移除触控点对应的 TouchTarget 内的 pointerIdBits 记录
// 移除后如果 pointerIdBits = 0(即没有其他触控点记录)
// 则把该 TouchTarget从 TouchTarget 序列中移除
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
整个 dispatchTouchEvent 源码我是这么理解的:
首先对于 TouchEvent 来说,其成员变量中包含:
1、View child:被点击的子 View,即消耗事件的目标 View;
2、int pointerIdBits:为了区分多点触控时不同的触控点,每一个触控点都会携带一个 pointerId。而 pointerIdBits 即是所有被当前 View 消耗的触控点的 pointerId 的组合。即 pointerIdBits 包含一个或以上的 pointerId 数据;
3、TouchTarget next:记录的下一个 TouchTarget 对象,由此组成了 TouchTarget 序列。
所以, TouchTarget 的作用是记录一个 View 及其对应分发的触控点列表 pointerIdBits,且可以通过 next 与其他实例形成序列。TouchTarget 把消耗事件的 View 以序列方式保存,且记录各个 View 对应的触控点列表,以实现后续的事件分发。
接下来,可以具体分析源码逻辑了。
handled 标志是返回的结果,表示是否被消费,不继续分发,默认为否,继续分发。
actionMasked == MotionEvent.ACTION_DOWN 判断是否为 ACTION_DOWN 事件,如果是的话,代表一个新的事件序列开始。然后调用 cancelAndClearTouchTargets 方法取消并清除所有触摸 target,调用 resetTouchState 方法重置所有触摸状态,为新的事件序列做准备。
intercepted 用来标志是否被拦截。接着判断是否为 ACTION_DOWN 事件,或 mFirstTouchTarget 不为空。即两种情况:
1、只有当前为 ACTION_DOWN 事件。
2、或者当 ACTION_DOWN 事件被子 View 消费后处理其他时会调用该代码,因为此时 mFirstTouchTarget!=null(mFirstTouchTarget 为 TouchTarget 序列中的第一个对象,mFirstTouchTarget 为 null 表示 TouchTarget 序列是空的)。
如果满足条件,就根据 disallowIntercept(表示是否不允许拦截),如果允许拦截,那么调用 onInterceptTouchEvent(ev) 方法进行事件拦截的判断,否则直接将 intercepted 设为 false,表示不被拦截;如果不满足条件,即不存在消耗 ACTION_DOWN 事件的目标控件时,那么后续的事件就直接将 intercepted 设为 true,表示进行拦截。从这里可以知道,如果子 View 没有消费 Touch 事件,那么当后续的事件到来时在这里直接返回 true。所以当我们当点击 ViewGroup 的空白位置时,由于不存在消耗 ACTION_DOWN 事件的子控件,导致 mFirstTouchTarget 为空,任何后续的事件到来时(包括多点触控 ACTION_POINTER_DOWN 事件),intercepted 都会被设为 true 而被 ViewGroup 拦截。所以,当 ViewGroup 决定拦截事件后,那么后续的点击事件也将会默认交给它处理,不再调用 onInterceptTouchEvent() 判断是否需要拦截。
之后在 if (!canceled && !intercepted) 判断的代码块中,如果当前事件没有被取消或拦截的情况,那就要开始处理事件分发了。
1、首先对事件类型进行区分,先处理 ACTION_DOWN 或者 ACTION_POINTER_DOWN 事件。
先记录触控点下标和 PointerId,然后检查是否有记录过 PointId 的 TouchTarget 存在(存在则移除),接下再开始遍历子 View。
通过一系列过滤找到合适的子 View 。
找到子 View 后,先通过 getTouchTarget 方法获取 TouchTarget 序列中是否已经存在包含该子 View 的 TouchTarget,如果已经存在,就直接把 idBitsToAssign 添加到该 TouchTarget 中。
之后调用 dispatchTransformedTouchEvent 方法并将子 View 作为参数传入,如果返回 true,表示事件传递给子 View,子 View 需要消费该事件,此时先通过 addTouchTarget 方法生成一个新的 TouchTarget(包含该需要消费事件的 View),并添加到了 TouchTarget 序列的头部,同时赋值给 newTouchTarget,然后将 alreadyDispatchedToNewTouchTarget 标志设为 true。
如果经过轮询子 View 的循环后没有找到消耗 ACTION_POINTER_DOWN 事件的 View,那就把 newTouchTarget 指向 TouchTarget 序列的最后的元素(即为消耗 ACTION_DOWN 事件的控件),并把当次 ACTION_POINTER_DOWN 事件的 PointID 记录到该元素。
2、接下来处理其他事件,当没有找到要接受事件的子 View 时,或者被拦截了,这里调用了 dispatchTransformedTouchEvent 方法并且传了一个 null 的 View 参数,即分发给 ViewGroup 自身;当有 View 接受了 ACTION_DOWN 事件,那么这个 View 也将接受其余的事件,其余事件也是通过递归调用 dispatchTransformedTouchEvent 方法分发。
3、如果是 ACTION_UP 和 ACTION_CANCEL 事件,通过 resetTouchState 方法进行清理和还原状态。如果是 ACTION_POINTER_UP 事件,那就移除触控点对应的 TouchTarget 内的 pointerIdBits 记录,移除后如果 pointerIdBits = 0(即没有其他触控点记录),则把该 TouchTarget从 TouchTarget 序列中移除。
可以发现,dispatchTouchEvent 方法中通过调用 dispatchTransformedTouchEvent 方法,将子 View 作为参数传入,下面就来看一看这个方法。
// ViewGroup.java
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
// 处理 ACTION_CANCEL
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;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 如果传来的参数 child 为空时,调用自身 dispatchTouchEvent() 方法
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// 如果传来的参数 child 不为空,那么就调用该 child 的 dispatchTouchEvent() 方法
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
// 如果传来的参数 child 为空时,调用自身 dispatchTouchEvent() 方法
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());
}
// 如果传来的参数 child 不为空,那么就调用该 child 的 dispatchTouchEvent() 方法
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
当方法中的 child 参数为空时,都会调用 super.dispatchTouchEvent(event) 方法,也就是将事件交给 ViewGroup 自己处理;否则调用 child.dispatchTouchEvent(event) 方法,即将事件分发给子 View。
综合上面的分析,所以当 child 为空时,即没有子 View 或子 View 不消费事件,ViewGroup 可以处理该事件,而当子 View 消费该事件时,ViewGroup 就无法调用 super.dispatchTouchEvent 方法处理事件。也就是说,子 View 消费事件,父 View 就无法消费该事件了。
一些小结
1、ViewGroup 的事件分发从 dispatchTouchEvent 方法开始。
2、ViewGroup 先调用 onInterceptTouchEvent 方法判断自己是否拦截事件。
1)如果 intercepted 为 true,自己拦截事件,则最后会调用自身的 super.dispatchTouchEvent 方法,最后自己处理该事件。
2)如果 intercepted 为 false,但是没有找到被点击的相应的子 View,那么最后会调用自身的 super.dispatchTouchEvent 方法,最后自己处理该事件。。
3)如果 intercepted 为 false,如果找到被点击的相应的子 View,那么会调用 child.dispatchTouchEvent 方法将事件传递给子 View。
3、当 ViewGroup 决定拦截事件后,后续的事件将会默认交给它处理,不再调用 onInterceptTouchEvent() 判断是否需要拦截。
4、ViewGroup 在遇到一个新的事件序列时,开始遍历自己的所有子 View,找到需要接收到事件的子 View。无论是否找到,最后都会调用 dispatchTransformedTouchEvent() 方法,区别在于如果找到了,那么在这个方法中传入的是那个子 View 对象,否则就为空。
5、没有子 View 或子 View 不消费事件,ViewGroup 可以处理该事件,而当子 View 存在且子 View 消费该事件时,ViewGroup 就无法调用 super.dispatchTouchEvent 方法处理该事件。
6、多点触摸同一个 View 时,释放后该 View 的点击事件只会触发一次。
4、View 事件分发源码分析
由上文可知,ViewGroup 自己处理事件,会调用 super.dispatchTouchEvent 方法;分发给子 View 处理事件,会调用 child.dispatchTouchEvent 方法,他们最终走到的都是 View 的 dispatchTouchEvent 方法。下面就来看看 View 的 dispatchTouchEvent 方法的源码。
// View.java
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
...
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
// 停止正在进行的嵌套滑动
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
// 过滤出应用安全策略的 Touch 事件
// 当 View 处于 enabled 状态,并且处理了通过鼠标输入拖动滚动条,就将 result 设为 true,表示消费该事件
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
// ListenerInfo 中封装了各种 Listener 监听,比如熟悉的 OnClickListener、OnLongClickListener 等
// 这里判断设置了 OnTouchListener 监听,且 onTouch 方法返回 true
// 则将 result 设为 ture,表示事件被消费
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 最后如果经过上面的过程 result 仍为 false,那么再去调用 onTouchEvent(event) 方法来消费该事件
// onTouchEvent 方法中如果消费该事件,则返回 true,此时 result 将会被设为 true
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
// 最后如果为 ACTION_UP、ACTION_CANCEL 事件或者为 ACTION_DOWN 事件但未消费事件,也停止正在进行的嵌套滑动
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
首先判断如果为 ACTION_DOWN 事件,那么调用 stopNestedScroll 方法停止正在进行的嵌套滑动。
然后过滤出应用安全策略的 Touch 事件后。
先判断为 enabled 状态,并且处理了鼠标输入拖动滚动条事件,那么 result 为 true。
接下来判断设置了 OnTouchListener 监听,且监听回调方法 onTouch 返回 true,即事件被消费,那么也将 result 设为 true。
最后如果经过上面的过程 result 仍为 false,那么再去调用 onTouchEvent(event) 方法来消费该事件,事件被消费的话 result 就会被设为 true。
接下来如果为 ACTION_UP、ACTION_CANCEL 事件或者为 ACTION_DOWN 事件但未消费事件,也停止正在进行的嵌套滑动。
最后返回结果 result。
可以知道,事件在 onTouchEvent(event) 方法中确定是否被消费。下面来看看 onTouchEvent 的源码。
// View.java
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
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 被设置为 disabled,
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
// clickable 的 View 虽然被设为了 disabled,但是仍然消耗触摸事件,只是不响应它们
// 所以这里返回 clickable
return clickable;
}
// 如果设置有 mTouchDelegate,则交由 TouchDelegate 处理。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 两种情况进入 if 代码块
// 1、当 View 为 clickable 时
// 2、或者 View 有 TOOLTIP,表示此 View 可以在悬停或长按时显示工具提示。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
// 下面便是针对不同的事件进行相应的处理
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
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) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
// 当前为点击事件的情况
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
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(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
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:
if (clickable) {
drawableHotspotChanged(x, y);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
final float ambiguousMultiplier =
ViewConfiguration.getAmbiguousGestureMultiplier();
if (!pointInView(x, y, touchSlop)) {
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* ambiguousMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= ambiguousMultiplier;
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
onTouchEvent 方法主要负责处理 touch 事件的。如果使用此方法检测点击动作,则建议通过实现并调用 performClick() 方法,这样能确保系统行为的一致性。
首先获取 touch 事件的坐标、action、clickable 等信息。
然后处理 View 被设置为 disabled 的情况。clickable 的 View 虽然被设为了 disabled,但是仍然消耗触摸事件,只是不响应它们,所以直接返回 clickable。如果设置有 mTouchDelegate,则交由 TouchDelegate 处理。
接下来只要 View 为 clickable 或有 TOOLTIP 标签(表示此 View 可以在悬停或长按时显示工具提示),便开始针对不同事件类型进行处理,主要分为 ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE 四种情况。
在处理 ACTION_UP 事件时,会调用到 performClick 方法;在处理 ACTION_DOWN 事件时,会依次调用到 checkForLongClick -> checkForLongPress -> performLongClick(mX, mY) -> performLongClick() -> performLongClickInternal(mLongClickX, mLongClickY) 方法。
最后可以看一下 performClick 方法和 performLongClickInternal 方法的源码。
// View.java
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
// NOTE: other methods on View should not call this method directly, but performClickInternal()
// instead, to guarantee that the autofill manager is notified when necessary (as subclasses
// could extend this method without calling super.performClick()).
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event,
* optionally anchoring it to an (x,y) coordinate.
*
* @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
* to disable anchoring
* @return {@code true} if one of the above receivers consumed the event,
* {@code false} otherwise
*/
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if ((mViewFlags & TOOLTIP) == TOOLTIP) {
if (!handled) {
handled = showLongClickTooltip((int) x, (int) y);
}
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
performClick 方法中,先检查 ListenerInfo 中是否设置 OnClickListener 回调,然后调用 onClick 回调方法。performLongClickInternal(float x, float y) 方法中,会先检查 ListenerInfo 中是否设置 OnLongClickListener 方法,然后调用 onLongClick 回调方法。所以,平时常用的 onClick 、onLongClick 回调方法是在 View 的 onTouchEvent 方法中处理的。
一些小结
1、View 的事件分发过程从 View 的 dispatchTouchEvent 方法开始。
2、View 的事件分发过程没有 onInterceptTouchEvent 方法参与,所以 onInterceptTouchEvent 方法只是 ViewGroup 事件分发过程中存在的方法。因为 View 没有子 View,所以不存在需要拦截事件的需求。
3、View 的 enable 属性不影响 onTouchEvent 的默认返回值。
4、平时常用的 onClick 、onLongClick 回调方法是在 View 的 onTouchEvent 方法中处理的。
5、View 的 onTouchEvent 方法默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为false)。