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

image

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

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

情景分析

image

上图中:

  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。

image

原理分析

上一节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标志位,可以控制在事件流过程中,突然弹出窗口后,后续事件流是否还能继续处理。

image

接下来是获取当前触摸事件的类型,可以看到这里获取了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是一个链表结构,保存了事件传递的子一系列目标View
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            // 检查View是否设置了暂时不在接收事件的标志位,如果有清除该标志位
            // 这样该View就能够接收下一次事件了。
            resetCancelNextUpFlag(target.child);
            // 将这个取消事件传给子View
            dispatchTransformedTouchEvent(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的成员变量,用来记录当前触摸目标链表的起始对象。

image

咱们接着看下一个方法。

private void resetTouchState() {
    // 再清除一次事件传递链中的View
    clearTouchTargets();
    // 再次清除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 {
        // 不允许拦截就直接设置为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.
    // 如果没有子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,
    // 有就获取它,没有就为null
    View 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默认为0
    final 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会往后排。

image

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

// 开始遍历子View啦,从后往前遍历
for (int i = childrenCount - 1; i >= 0; i--) {
    // 7.0就是i
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    // 如果preorderedList不为空,从preorderedList中取View
    // 如果preorderedList为空,从mChildren中取View
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);
    // 如果当前已经有View获得焦点了,找到它。后面的触摸事件会优先传给它。
    // 应该主要影响后面触摸点的Down事件
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        // 找到后i设为最后一个View
        i = 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 index
        for (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();
    // 创建一个TouchTarget
    newTouchTarget = 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;
    // 先记录原本的Action
    final 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,将这个取消事件传递给子View
            handled = 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;
    ...
    // 获取触摸事件的触摸点id
    final 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的。

image

我们回到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,设置处理标记为true
            handled = true;
        } else {
            // 父View拦截,或者child原本不可接收TouchEvent的状态,为true
            final 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 ,以加深对触摸式的整体印象。

image

根据上面分析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的【个人主页】点个关注就好了哦。发车喽~
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容