在CoorChice的这篇文章《原来Android触控机制竟是这样的?》http://www.jianshu.com/p/b7cef3b3e703 中,CoorChice简要的介绍了一下Android中触摸事件的大致流程。于做应用而言,实际我们只需要清楚文中蓝色那部分流程就行。

本篇文章中,CoorChice将针对这个流程进行分析。为什么这个流程会是这个样子?以及这个流程中有什么特别之处。

情景分析

上图中:

  1. VG表示最底层父。
  2. VG-1包含一个子View V-1-1。
  3. VG-1、V-2、V-3都是VG的子View,且V-3是覆盖在V-2上的。

假设VG-1的onInterceptTouchEvent()中,会拦截上下滑动的事件,而不会拦截左右滑动事件。V-1-1能够处理左右滑动事件。

情境一 在point-1点长按V-1-1触发长按事件的过程中,发生了左右滑动,长按事件还能触发吗?

在View的onTouchEvent()的默认实现中,长按事件是在接收到ACTION_DOWN之后,post一个延时500ms的Runnable实现对象。如果顺利的话,500ms后View会调用给它设置的onLongClickListeneronLongClick()方法,如果这个方法返回了true,则记录下长按事件已经触发了。

在等待的500ms中,View还会不断的接收到TouchEvent事件,触发ACTION_MOVE,如果触发了滑动(这里假设触发的是左右滑动事件),就会移除前面post的Runnable实现对象。所以长按事件就不会触发。

情景二 接着上面左右滑动的过程,如果再发生上下滑动,VG-1的上下滑动会触发吗?

由于每个事件的传递都会经过VG-1的dispatchTouchEvent()方法才能分发到V-1-1的onTouchEvent()中,而一般情况下,在VG-1的dispatchTouchEvent()中每次都会询问onInterceptTouchEvent()要不要拦截事件,如果拦截了,事件自然就会再往下分发了。

所以,在V-1-1左右滑动中,再发生上下滑动,事件将会被VG-1拦截,然后触发它的上下滑动。

情景三 接着情景二,如果再次发生左右滑动,V-1-1还能触发左右滑动吗?

VG-1一旦拦截了触摸事件,就会给所有TouchTarget中的子View模拟分发一个ACTION_CANCEL事件,将原本能接收到触摸事件的子View排除在本次事件流之外。并且会清空TouchTarget队列。这样后续的事件将会跳过onInterceptTouchEvent()判断,直接标记为拦截状态,进而分发到VG-1自己的onTouchEvent()中去。

所以,这种情况下V-1-1将不能再接收到本次事件流中的事件了。就是说,只要父View拦截了事件,子View就再也拦截不到事件了,并且后续的事件不管父View拦不拦截,都会往父View的onTouchEvent()中分发。

情景四 在情景一的分析中我们知道,在V-1-1接收到ACTION_DOWN时会post一个延迟Runnable等待触发长按事件。那么是不是即使我们让V-1-1在接收到ACTION_DOWN时返回false,等到500ms后,长按事件还会触发吗?

我们知道,如果V-1-1的onTouchEvent()返回了false,表示它不处理事件,那么它将不会保存到VG-1的TouchTarget队列中。就是说即使下一个事件将被VG-1直接拦截,也不会有ACTION_CANCEL事件分发到V-1-1中。而长按事件的Runnable除了上面说的情况会被移除外,收到ACTION_CANCEL时也会移除。所以这种情况下V-1-1的长按事件Runnable是没办法被移除的,只要到500ms,它的长按事件就会触发。

情景五 在情景一 的基础上,如果V-1-1向右滑动了一定距离后onTouchEvent()返回false,当在point-1再滑动一定距离,然后让onTouchEvent()再次返回true。那么V-1-1还能继续接收到后续是事件吗?

在本情景下,当V-1-1的onTouchEvent()返回false后,由于没有触发上下滑动,所以VG-1不会拦截触摸事件。再就是前面已经把V-1-1放到了VG-1的触摸目标队列中,所以触摸事件仍然能够分发到V-1-1中,即使它返回了false。

所以,这种情况下,V-1-1始终能接收到事件。

情景六 触摸point-2点,如果ACTION_DOWN时,V-3的onTouchEvent()返回false,V-2的的onTouchEvent()返回true,V-3设置为随后再有事件分发过来时返回true,V-3还能继续处理后续事件吗?

经过上面几个情景的分析我们知道,在ACTION_DOWN时VG-1会从后往前遍子View,发现有子View消费了事件的,就将它存入TouchTarget中,然后不在进行后面的遍历。在这个情景中,V-3在ACTION_DOWN时不处理事件,这意味着它就没有被存到TouchTarget中,而后续的事件不会再进行遍历,而是直接分发到TouchTarget对应的子View中,所以本次事件流将会在V-2中处理。而V-3将不能再收到后面的事件。

这就是为什么有的View它默认实现的onTouchEvent()会在ACTION_DOWN时返回false,从而出现"点击穿透"的Bug。

原理分析

上一节CoorChice通过六个情景再现了一些我们平时会遇到的触摸事件场景。下面CoorChice将从原理上分析,这些情景中的结果是怎么来的?

其实要理解上层的触摸事件机制,关键就在于ViewGroup的dispatchTouchEvent(),它实现了Android事件分发的一套逻辑,当然如果我们有更好的方法,也可以自己来处理这个过程!但是现在我们还是基于Android的逻辑来看吧。

下面的逻辑基本都是ViewGroup的dispatchTouchEvent()中的,期间也会涉及到相关方法的分析,如遇到没写方法名的,表明这段代码是在ViewGroup的dispatchTouchEvent()中的。

1. 判断要不要分发本次触摸事件

...
if (onFilterTouchEventForSecurity(ev)) {// 获取MotionEvent的事件类型final int action = ev.getAction();// actionMasked能够区分出多点触控事件final int actionMasked = action & MotionEvent.ACTION_MASK;...

首先通过onFilterTouchEventForSecurity(ev)检查要不要分发本次事件,检查通过了才会进行分发,走if中的逻辑。否则就放弃对本次事件的处理。

我们看看这里是依据什么条件来判断要不要进行分发的。

public boolean onFilterTouchEventForSecurity(MotionEvent event) {if (// 先检查View有没有设置被遮挡时不处理触摸事件的flag(mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0// 再检查受到该事件的窗口是否被其它窗口遮挡&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {// Window is obscured, drop this touch.return false;}return true;
}

看代码注释,这里就是检查了两个条件,看这个事件对于该View来说是不是安全的。

第一个条件FILTER_TOUCHES_WHEN_OBSCURED可以通过在xml文件中的android:filterTouchesWhenObscured来设置,或者在Java中通过setFilterTouchesWhenObscured()来添加或移除。DecorView默认是没有这个标志位的,而其他View基本上默认都是有的。Android这样设计是为了让我们可以自主的选择要不要过滤不安全事件。如果我们让自己的View不过滤这样的事件,那么在一个事件流进行中,如果突然弹出一个新窗口,我们的View仍然能接收到触摸事件。

第二个条件是在本次触摸事件分发到ViewGroup所在窗口时,判断窗口如果处于被其它窗口遮挡的状态的话,就会给这个MotionEvent加上这个标志位。

知识点:通过设置或清除FILTER_TOUCHES_WHEN_OBSCURED标志位,可以控制在事件流过程中,突然弹出窗口后,后续事件流是否还能继续处理。

接下来是获取当前触摸事件的类型,可以看到这里获取了ACTION_MASK。action的值包括了触摸事件的类型和触摸事件的索引(Android中在一条16位指令的高8位中储存触摸事件的索引,在低8位中储存触摸事件的类型),而actionMask仅仅包括事件类型。对于多点触控,我们需要获取到索引信息才能区分不同的触摸点,进行操作。

2. ACTION_DOWN会重置状态

if (actionMasked == MotionEvent.ACTION_DOWN) {// 清理上一次接收触摸事件的View的状态cancelAndClearTouchTargets(ev);// 重置ViewGroup的触摸相关状态resetTouchState();
}

这一步会先判断是不是ACTION_DWON事件,如果是的话需要进行一些重置,防止对新的事件流产生影响。下面我们看看上段代码中调用的两个重置方法吧。

private void cancelAndClearTouchTargets(MotionEvent event) {// 如果触摸事件目标队列不为空才执行后面的逻辑if (mFirstTouchTarget != null) {boolean syntheticEvent = false;if (event == null) {final long now = SystemClock.uptimeMillis();// 自己创建一个ACTION_CANCEL事件event = MotionEvent.obtain(now, now,MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);// 设置事件源类型为触摸屏幕event.setSource(InputDevice.SOURCE_TOUCHSCREEN);// 标记一下,这是一个合成事件syntheticEvent = true;}// TouchTarget是一个链表结构,保存了事件传递的子一系列目标Viewfor (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {// 检查View是否设置了暂时不在接收事件的标志位,如果有清除该标志位// 这样该View就能够接收下一次事件了。resetCancelNextUpFlag(target.child);// 将这个取消事件传给子ViewdispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);}// 清空触摸事件目标队列clearTouchTargets();if (syntheticEvent) {// 如果是合成事件,需要回收它event.recycle();}}
}

cancelAndClearTouchTargets(ev)流程不长,它就是清除上一次触摸事件流中能够接收事件的所有子View的PFLAG_CANCEL_NEXT_UP_EVENT标志,并且模拟了一个ACTION_CANCEL事件分发给它们。

一个View如果有PFLAG_CANCEL_NEXT_UP_EVENT标志,表示它跟ViewGroup解除了绑定,通常会在调用ViewGroup#detachViewFromParent(View)后被添加。一般不会有这个标记的。

给上一次接收事件流的子View发送模拟的ACTION_CANCEL事件,可以重置这些子View的触摸状态。比如取消它们的点击或者长按事件。

这里先讲一下能够接收触摸事件流的子View怎么被记录的。其实就是使用一个TouchTarget去记录,它是一个单链表结构,并且有复用机制,设计的比较巧妙。下面是TouchTarget中的与我们关连最大的两个成员。

// 用来保存能够处理触摸事件的View
public View child;
// 指向下一个TouchTarget
public TouchTarget next;

不难看出,每一个能够接收触摸事件流的子View都对应着一个TouchTarget。

后面CoorChice就称这个链表为触摸目标链表了。

mFirstTouchTarget是ViewGroup的成员变量,用来记录当前触摸目标链表的起始对象。

咱们接着看下一个方法。

private void resetTouchState() {// 再清除一次事件传递链中的ViewclearTouchTargets();// 再次清除View中不接收TouchEvent的标志resetCancelNextUpFlag(this);// 设置为允许拦截事件mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;mNestedScrollAxes = SCROLL_AXIS_NONE;
}

这个方法后中就是把触摸链表清空了,然后清除ViewGroup的PFLAG_CANCEL_NEXT_UP_EVENT标志,如果有的话。然后清除FLAG_DISALLOW_INTERCEPT标志(这个标志后面在详细说)。

到此,我们的ViewGroup及其子View们就准备好迎接新的触摸事件了!

3. 检查ViewGroup要不要拦截事件

// 这个变量用于检查是否拦截这个TouchEvent
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {// 检查是否不允许拦截事件final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {// 如果没有不允许拦截,就是允许拦截。// 调用onInterceptTouchEvent看ViewGroup是否拦截事件intercepted = onInterceptTouchEvent(ev);// 在这里防止Event中途被篡改// 所以要篡改TouchEvent的Action不要在这之前改ev.setAction(action); // restore action in case it was changed} else {// 不允许拦截就直接设置为falseintercepted = false;}
} else {// There are no touch targets and this action is not an initial down// so this view group continues to intercept touches.// 如果没有子View,并且也不是ACTION_DOWN事件,直接设置为拦截// 这样后面就自己处理事件intercepted = true;
}

第一个if中的判断限制了,必须是ACTION_DOWN事件,或者mFirstTouchTarget != null才会询问onInterceptTouchEvent()要不要拦截事件,否则ViewGroup就直接拦截了。

现在回过头看看情景四,之所以会出现就是因为在ACTION_DOWN的时候没有V-1-1没有处理事件,导致这里的mFirstTouchTarget == null,再加上后续事件类型不再是ACTION_DOWN了,所以ViewGroup就默认把后面的事件都拦截了。

在第一个if的逻辑中,我们看到它先检查ViewGroup是否有FLAG_DISALLOW_INTERCEPT标志,如果有就直接不进行拦截,如果没有才会询问onInterceptTouchEvent()要不要拦截。这个标志在上一步中也出现了,只不过是清理掉它,所以ACTION_DOWN发生时,ViewGroup肯定是没这个标志的。那什么时候可能会有呢?只有在有子View处理触摸事件流的过程中,有子View调用requestDisallowInterceptTouchEvent(),可以给ViewGroup添加这个标志位。

知识点:如果子View在ACTION_DWON时处理了事件,那么后面可以通过requestDisallowInterceptTouchEvent(true)来禁止父View拦截后续事件。

一个纵向滑动的RecyclerView嵌套一个横向滑动的RecyclerView时之所以在触发横向滑动后,就再不能触发纵向滑动,就是因为RecyclerView在发生横滑时调用了requestDisallowInterceptTouchEvent(true)禁止父View再拦截事件。

4. 初始化用于后续判断的变量

// 标识本次事件需不需要取消
final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;
// 检查父View是否支持多点触控,即将多个TouchEvent分发给子View,
// 通过setMotionEventSplittingEnabled()可以修改这个值。
// FLAG_SPLIT_MOTION_EVENTS在3.0是默认为true的,即支持多点触控的分发。
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;

5. 判断是否要给子View分发事件

if (!canceled && !intercepted) {// 检查TouchEvent是否可以触发View获取焦点,如果可以,查找本View中有没有获得焦点的子View,// 有就获取它,没有就为nullView childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()? findChildWithAccessibilityFocus() : null;

从这个if可以看出,只有不需要发送取消事件,并且ViewGroup没有拦截事件时才会进行分发。

上面代码中,还尝试获取了一下ViewGroup中当前获得焦点的View,这个在后面的判断中会用到。

6. 判断是不是DOWN事件

// 可以执行if里面的情况:
// 1. ACTION_DOWN事件
// 2. 支持多点触控且是ACTION_POINTER_DOWN事件,即新的事件流的DOWN事件
// 3. 需要鼠标等外设支持,暂时无意义
// 就是说同一个事件流,只有Down的时候才会去寻找谁要处理它,
// 如果找到了后面的事件直接让它处理,否则后面的事件会直接让父View处理
if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 获取当前触摸手指在多点触控中的排序// 这个值可能因为有手指发生Down或Up而发生改变final int actionIndex = ev.getActionIndex(); // always 0 for down// 标识当前是那一个点的触摸事件final int idBitsToAssign = split ? 1 <<// 此时获取到手指的Id,这个值在Down到Up这个过程中是不会改变的ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS;// Clean up earlier touch targets for this pointer id in case they// have become out of sync.// 清理之前触摸事件中的目标removePointersFromTouchTargets(idBitsToAssign);

这个if判断当前的触摸事件类型是不是DOWN类型,如果是才会进入if开始真正的遍历给子View分发事件。也就是说,一个事件流只有一开始的DOWN事件才会去遍历分发事件,后面的事件将不再通过遍历分发,而是直接发到触摸目标队列的View中去。

在开始遍历前,还需要初始化一下多点触控的事件信息,看上面的代码。

7. 初始化子View集合

// 子View数量
final int childrenCount = mChildrenCount;
// 第一个点的Down事件newTouchTarget肯定为null,后面的点的Down事件就可能有值了
// 所以只有第一个点的Down事件时走if中逻辑
if (newTouchTarget == null && childrenCount != 0) {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放到集合中,按照添加顺序排序,但是受到Z轴影响// 只有子View数量大于1,并且其中至少有一个子View的Z轴不为0,它才不为null// 7.0中,View的elevation默认为0final ArrayList<View> preorderedList = buildTouchDispatchChildList();// 如果preorderedList为空,并且按炸后final boolean customOrder = preorderedList == null//检查ViewGroup中的子视图是否是按照顺序绘制,其实就是不受z轴影响&& isChildrenDrawingOrderEnabled();final View[] children = mChildren;

这一步中,获取了触摸事件的坐标,然后初始化了preorderedListmChildren两个子View集合。为什么需要两个呢?肯定是有所区别的。先看看preorderedList

通过buildTouchDispatchChildList()构建出来的子View集合有如下特点:

  1. 如果ViewGroup的子View数量不多于一个,为null;
  2. 如果ViewGroup的所有子View的z轴都为0,为null;
  3. 子View的排序和mChildren一样,是按照View添加顺序从前往后排的,但是还会受到子Viewz轴的影响。z轴较大的子View会往后排。

mChildren上面说过,就是按照View添加顺序从前往后排的。

所以,这两个子View集合的最大区别就是preorderedList中z轴较大的子View会往后排。

8. 开始遍历寻找能够处理事件的子View

// 开始遍历子View啦,从后往前遍历
for (int i = childrenCount - 1; i >= 0; i--) {// 7.0就是ifinal int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);// 如果preorderedList不为空,从preorderedList中取View// 如果preorderedList为空,从mChildren中取Viewfinal View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);// 如果当前已经有View获得焦点了,找到它。后面的触摸事件会优先传给它。// 应该主要影响后面触摸点的Down事件if (childWithAccessibilityFocus != null) {if (childWithAccessibilityFocus != child) {continue;}childWithAccessibilityFocus = null;// 找到后i设为最后一个Viewi = childrenCount - 1;}

可以看到,对子View集合的遍历是倒序的。这就是为什么覆盖在上层的View总是能有限获取到事件的原因。

通过getAndVerifyPreorderedView()preorderedList或者mChildren中取到一个子View。然后如果ViewGroup中有子View获得了焦点,那么就会去找到这个获得焦点的子View。注意,如果找到了会执行 i = childrenCount - 1,这意味for中的逻辑执行完就结束了,如果中途有continue的操作,就直接退出循环。

9. 检查事件是否在View内

// 检查View是否显示或者播放动画以及TouchEvent点是否在View内
// 如果不满足会继续寻找满足的子View
if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;
}

如果上一步找到了获取焦点的View,并且触摸事件又没在焦点View中,退出循环。

如果没有任何的子View包含触摸点,也退出循环。

如果没有View获得焦点,就直到找到包含触摸点的View才会继续往下执行。

10. 进行DOWN事件的分发

// 再次重置View
resetCancelNextUpFlag(child);
// 将事件传给子View,看子View有没有消费,消费了执行if中逻辑,并结束循环。
// 就是说该View之后的子View将都不能接收到这个TouchEvent了
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 记录这次TouchEvent按下的时间mLastTouchDownTime = ev.getDownTime();if (preorderedList != null) {// childIndex points into presorted list, find original indexfor (int j = 0; j < childrenCount; j++) {if (children[childIndex] == mChildren[j]) {// 记录触摸的View的位置// 然后结束本次事件传递mLastTouchDownIndex = j;break;}}} else {// 记录触摸的View的位置mLastTouchDownIndex = childIndex;}mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();// 创建一个TouchTargetnewTouchTarget = addTouchTarget(child, idBitsToAssign);// 标记已经把事件分发给了newTouchTarget,退出子View遍历alreadyDispatchedToNewTouchTarget = true;break;
}

再次强调!从第6步到这段逻辑只有DOWN类型的事件才能执行,其它类型的事件是不会走这的!

一开始再一次的尝试清除子View的PFLAG_CANCEL_NEXT_UP_EVENT标志。然后在给子View分发事件,如果子View处理了,走if中逻辑,如果没处理,继续寻找下一个满足条件的View,然后看它处不处理。

这里插入讲解一下dispatchTransformedTouchEvent()方法。这个方法在第一步的cancelAndClearTouchTargets()方法中也出现过,当时是用它来给子View分发取消事件。下面同样分段来讲解这个方法。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;// 先记录原本的Actionfinal int oldAction = event.getAction();if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {// 可能过来的事件没有ACTION_CANCEL,如果希望取消的话,那么为事件添加取消标志。event.setAction(MotionEvent.ACTION_CANCEL);if (child == null) {// 如果没有子View了,调用View中的dispatchTouchEvent// 进而调用View的onTouch或者onTouchEvent方法,触发ACTION_CANCEL逻辑handled = super.dispatchTouchEvent(event);} else {// 如果有子View,将这个取消事件传递给子Viewhandled = child.dispatchTouchEvent(event);}// 在设置回原本的Action// 此时TouchEvent的行为相当于没变// 但是却把该ViewGroup的event.setAction(oldAction);return handled;}...

这一段代码实现的功能就是:如果需要分发取消事件,那么就会分发取消事件。如果有目标子View则将取消事件分发给目标子View,如果没有就分发给ViewGroup自己,走的是View的dispatchTouchEvent(),和普通View一样。

如果不需要分发取消事件,就走下面一般事件的分发流程。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {final boolean handled;...// 获取触摸事件的触摸点idfinal int oldPointerIdBits = event.getPointerIdBits();// 看和期望的触摸点是不是一个final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;if (newPointerIdBits == 0) {// 表示不是return false;}final MotionEvent transformedEvent;// 如果是同一个触摸点,进入if逻辑if (newPointerIdBits == oldPointerIdBits) {if (child == null || child.hasIdentityMatrix()) {if (child == null) {// 如果不给子传事件,自己在onTouch或onTouchEvent中处理handled = super.dispatchTouchEvent(event);} else {final float offsetX = mScrollX - child.mLeft;final float offsetY = mScrollY - child.mTop;// 根据滚动值计算触摸事件的偏移位置event.offsetLocation(offsetX, offsetY);// 让子View处理事件handled = child.dispatchTouchEvent(event);// 恢复TouchEvent坐标到原来位置,避免影响后面的流程event.offsetLocation(-offsetX, -offsetY);}return handled;}transformedEvent = MotionEvent.obtain(event);} else {transformedEvent = event.split(newPointerIdBits);}...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());}// 让子View处理事件handled = child.dispatchTouchEvent(transformedEvent);}// Done.transformedEvent.recycle();return handled;
}

开始先判断所指的是不是同一个触摸点,如果是的话再判断传入的child为不为空,或者传入的child的变换矩阵还是不是单位矩阵。如果满足再看传入的child为不为null,如果为空说明需要让ViewGroup去处理事件,反之将把事件分发给child处理。

如果是把事件分发给child的话,接下来会计算事件的偏移量。因为child在ViewGroup中可能会发生位置变化,需要除去这些移动距离,以保证事件到达child的onTouchEvent()中时,能够正确的表示它在child中的相对坐标。就相当于事件也要跟着child的偏移而偏移。

可以看到,在进行事件传递时,会根据本次触摸事件构建出一个临时事件transformedEvent,然后用临时事件去分发。这样做的目的是为了防止事件传递过程中被更改。

所以,这个方法主要就是用来根据参数把一个事件分发到指定View的。

我们回到dispatchTouchEvent()中继续。如果if中的dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)返回true,表示子View处理了本事件,那么接着会创建一个TouchTarget,并且关联该子View,后续的触摸事件就会通过这个TouchTarget取出子View,直接把事件分发给它。我们看看这里TouchTarget的创建。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);target.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;
}

注意啦,创建之后还给mFirstTouchTarget赋值了,从此,mFirstTouchTarget就不为空了。

最后,在有子View消费本次Down事件后,会执行alreadyDispatchedToNewTouchTarget = true,标记一下已经有子View消费了事件。然后退出循环遍历,其余还没遍历到的子将收不到该触摸事件。

11. 处理多点触控

// newTouchTarget在不是Down事件,或者没有找到处理事件的View时是null
// mFirstTouchTarget在Down事件时,如果找到了处理的View就不为null
if (newTouchTarget == null && mFirstTouchTarget != null) {// 直接让上次处理的View继续处理newTouchTarget = mFirstTouchTarget;while (newTouchTarget.next != null) {newTouchTarget = newTouchTarget.next;}newTouchTarget.pointerIdBits |= idBitsToAssign;
}

if中的代码只有在多点触控中才能执行。

12. 处理后续事件

if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.// 父View自己处理事件handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {TouchTarget predecessor = null;TouchTarget target = mFirstTouchTarget;while (target != null) {final TouchTarget next = target.next;// 只有down时,并且有View处理了事件才会走if中逻辑if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// 表示触摸事件已经被子View处理,并且找到了子View,设置处理标记为truehandled = true;} else {// 父View拦截,或者child原本不可接收TouchEvent的状态,为truefinal boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;// 如果事件被父View拦截了,或者child原本被打上了暂时不可接收TouchEvent的标记PFLAG_CANCEL_NEXT_UP_EVENT// 给它发送取消事件// 如果父View没有拦截,并且子View原本没有PFLAG_CANCEL_NEXT_UP_EVENT// 给它分发事件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;}
}

如果Down事件没有子View处理,mFirstTouchTarget肯定会为null,所以在这里把事件分发给ViewGroup的onTouchEvent()自己处理。

如果Down事件有子View处理,那么会通过alreadyDispatchedToNewTouchTarget看是不是接着上面找到处理事件的子View的逻辑,如果是的话直接标记本次Down事件已经被处理。

如果是非Down事件,则通过TouchTarget拿到能够处理事件的View,然后给它分发事件。

上面的代码中,需要注意这里:

final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
...
if (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;
}

这段代码实现的功能是,如果ViewGroup中途拦截了事件,即intercepted为true。那么就会将触摸事件队列"清空"。这样就会使后续的触摸事件直接被ViewGroup默认拦截。看看第三步那的逻辑哦!

知识点:如果父View中途拦截了子View的触摸事件流,那么事件流中的后续事件将都被父View拦截,并且不能取消拦截。

总结

从上文的分析可以看出,Android应用层的事件分发机制主要依靠的是ViewGroup的dispatchTouchevent()方法的实现。我们只有充分的了解了这个过程才能治在开发过程中遇到的各种不服!大家看完这篇文章可以再回过头看下CoorChice的这篇文章《原来Android触控机制竟是这样的?》http://www.jianshu.com/p/b7cef3b3e703 ,以加深对触摸式的整体印象。

根据上面分析CoorChice认为,以下几点是我们在处理事件分发时需要注意的:

  1. 如果一个View想要处理触摸事件,需要在onTouchEvent()onTouch()中接收ACTION_DOWN时返回true,通知父View。否则该事件流中的后续事件就不会再往该View中分发了。
  2. 如果一个父View拦截了一个事件,那么从这个事件开始之后的事件都不会在分发到子View中,同时父View也不会再询问onInterceptTouchEvent()是否拦截,而是直接拦截事件。并且会给之前处理事件的子View分发一个ACTION_CANCEL事件,让子View停止处理事件。
  3. 在父View拦截事件前,可以子View可以通过requestDisallowInterceptTouchEvent()方法给父View添加FLAG_DISALLOW_INTERCEPT标志,使父View不能再拦截事件。
  4. 通过setFilterTouchesWhenObscured()或者在xml中设置android:filterTouchesWhenObscured来自行决定要不要在窗口被遮挡时继续处理事件。
  5. 触摸事件的分发是按View添加的顺序逆序分发的,所以在上层的View总能优先收到事件。如果ViewGruop设置了FLAG_USE_CHILD_DRAWING_ORDER标志,即开启按z轴顺序绘制,则z轴值最大的子View将优先收到事件。
  • 抽出空余时间写文章分享需要动力,还请各位看官动动小手点个赞,给CoorChice点鼓励?
  • CoorChice一直在不定期的分享新的干货,想要上车只需进到CoorChice的【个人主页】点个关注就好了哦。发车喽~

转载于:https://my.oschina.net/coorchice/blog/2999108

你还在被触摸事件困扰吗?看看这篇吧相关推荐

  1. h5滚动时侧滑出现_HTML5移动端触摸事件以及滑动翻页效果

    HTML5中新添加了很多事件,但是由于他们的兼容问题不是很理想,应用实战性不是太强,所以在这里基本省略,咱们只分享应用广泛兼容不错的事件,日后随着兼容情况提升以后再陆续添加分享.今天为大家介绍的事件主 ...

  2. Android12特性“不受信任的触摸事件被屏蔽”其他细节

    Android12特性"不受信任的触摸事件被屏蔽"其他细节 android12-release Android 12关于Input触摸事件的行为变更 这篇介绍了input实际的拦截 ...

  3. cocos2d ccLayer响应触摸事件方法:CCStandardTouchDelegate 与 CCTargetedTouchDelegate

    cocos2d ccLayer响应触摸事件方法:CCStandardTouchDelegate 与 CCTargetedTouchDelegate    以下内容转载自:http://blog.sin ...

  4. h5滚动时侧滑出现_H5触摸事件中如何判断用户滑动方向

    这次给大家带来H5触摸事件中如何判断用户滑动方向,H5触摸事件中判断用户滑动方向的注意事项有哪些,下面就是实战案例,一起来看一下. 接口 TouchEvent TouchEvent 是一类描述手指在触 ...

  5. Android开发--事件的处理/按键按下,弹起,触摸事件等

    android中的事件类型分为按键事件和屏幕触摸事件 事件是我们在于UI交互式发生的,我们点击一个按键时,可能就已经除非好几个事件,例如我们点击数字键"0",他会涉及到按下事件,和 ...

  6. cocos2dx3.x使用cocostudio触摸事件不响应的奇葩问题

    cocos2dx3.x使用cocostudio触摸事件不响应的奇葩问题 刚刚使用3.1,发现了一些关于触摸的不同之处,对于习惯于2.x的人还是认为坑啊,简单总结一下: 使用cocostudio加进来的 ...

  7. WP7开发—Silverlight多点触摸事件详解【含Demo代码】

    最近在学习WP7的Silverlight编程,就把学习到知识点整理为日志,方便自己理解深刻点,也作为笔记和备忘,如有不合理或者错误之处,还恳请指正. WP7的支持多点触摸,有两种不同的编程模式: 1. ...

  8. 移动端触屏网页的触摸事件

    PC端网页从无到有发展至今,人们习惯了鼠标与键盘的人机交互模式,因此在PC端网页开发中一般使用鼠标事件和键盘事件. mouse事件: onclick事件:在单击鼠标左键或右键时发生. ondouble ...

  9. html5里可移动线性进度条的类型怎么表示,HTML5触摸事件实现移动端简易进度条的实现方法...

    这篇文章主要介绍了关于HTML5触摸事件实现移动端简易进度条的实现方法,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下 前言 HTML中新添加了许多新的事件,但由于兼容性的问题,许多事件 ...

最新文章

  1. SVM原理详细图文教程来了!一行代码自动选择核函数,还有模型实用工具
  2. 从本地的win传文件到本地的linux上,pscp.exe实现本地windows下的文件下载(传输)到linux上...
  3. 使用gdbserver远程调试
  4. 一个项目中既有移动端,同时也有PC端的代码,并且 他们的代码分开写的,那么如何实现在手机跳转手机页面,pc点击跳转pc页面...
  5. 机器学习(K-means聚类原理以及用法)
  6. mysql io_MySQL服务器 IO 100%的分析与优化方案
  7. python3 音乐播放器_Python3——MP3播放器
  8. python启动文件_Python启动文件配置
  9. android:xml属性集
  10. 怎么在Linux中telnet服务器,Linux系统下Telnet服务器配置
  11. 大数据讲课笔记2.1 初探大数据
  12. PS怎样去掉图片上的文字
  13. 密码学——对称加密加密模式
  14. 12306抢票JS脚本
  15. java反射机制是什么_java的反射机制是什么?
  16. python运维自动化老男孩_老男孩第十四期 python 自动化运维第二周
  17. SharePoint加K2,将Portal系统与BPM系统完美整合!
  18. 软考高级系统架构设计师论文系列三:论改进Web服务器性能的有关技术
  19. java源程序文件_.class文件为Java源程序文件
  20. S3C6410移植linux4.17内核(一)

热门文章

  1. 大白话、最简单——SpringBoot+Mybatis+freemarker整合(二)
  2. 磁力搜索网站+下载神器放送2019-03-05
  3. html文件右键没有打开方式,一个文件打不开,点右键,怎么在打开方式中加入Word,Excel的打开方式,打开方式中有Word的打开方式?...
  4. Ubuntu 环境部署 安装大全
  5. 'pip' 不是内部或外部命令,也不是可运行的程序或批处理文件。
  6. 【生活】教你有效戒糖
  7. 流量矿石团队成员出席“区块链技术与金融领域创新应用培训会”
  8. 今日干货:mac视频剪辑软件推荐
  9. 快速查询出中通快运物流信息,将信息导出EXCEL表格
  10. 基于java web和echarts的数据可视化项目