安卓 触摸事件分发机制

基本使用

对于事件处理,主要涉及4个重写方法1个调用方法

3个控件的方法重写:

Java 复制代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return super.onInterceptTouchEvent(ev);
}
 
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    return super.dispatchTouchEvent(ev);
}
 
@Override
public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
}

1个注册OnTouchListener

Java 复制代码
boolean onTouch(View v, MotionEvent event);

对于 OnClickListener OnLongClickListener 等是基于touch事件的,本文就以OnTouchListener为代表讲解

1个主动调用方法:

Java 复制代码
getParent().requestDisallowInterceptTouchEvent(true);

结论写在前面

结论1

(dispatchTouchEvent拦截事件的优先级高于onTouch/onTouchEvent)

从根控件向子控件方向,出现的第一个dispatchTouchEvent返回值为false的控件(重写方法设置返回值false),他的父控件到根控件才是有效的事件分发范围,换句话说,从这个false控件开始到最上层叶子控件这个范围的控件,都不会再被分配本组事件的后续事件了。(通常从down事件到up事件/cancel事件为一组完整的事件,下一组事件再由down事件开始判定、分发)

down事件:

从根布局控件开始遍历到叶子控件,所有的dispatchTouchEvent都被调用了。

onTouch调用:发现最上层叶子控件和一个dispatchTouchEvent返回false的控件的父控件这两个控件的onTouch被回掉了。实际是子控件无效(为空或者dispatchTouchEvent返回false)的控件会被回调onTouch。原因后续会讲解代码会讲。

move事件:

由down事件决定,down事件中从根布局到叶子布局的方向第一个出现dispatchTouchEvent返回false的控件开始往后的控件就都不被分发事件了。

结论2

在不重写dispatchTouchEvent去影响其流程及返回值的前提下,只重写onTouch/onTouchEvent方法控制消费事件,从最外侧子控件向根布局方向,出现第一个onTouch/onTouchEvent返回值为true的控件就是消费事件的控件。

down事件:

如上图,按照DCBA的方向,第一个onTouch返回true的控件C消费事件。

move事件:

move事件由down事件决定,只有控件C的onTouch被回调

事件分发源码分析

根据input机制,生成input事件最终会分配给Activity的根View执行其dispatchTouchEvent,我们就以dispatchTouchEvent为入口开始分析。

在分析dispatchTouchEvent之前,对其中调用的几个关键方法先做个介绍,方便后面主流程讲解时理解。

dispatchTransformedTouchEvent:

Java 复制代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ...
    if (child == null) {
        // super是View.java, dispatchTouchEvent主要是回调onTouchEvent系列方法
        // onTouchEvent默认实现的返回值由 CLICKABLE 决定
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}

只留下主要关注的代码,可以看出:

  1. 如果方法入参child为null时,就调用自己的super.dispatchTouchEvent,即view.dispatchTouchEvent

    Java 复制代码
    // View.java
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
        ...
        return result;
    }

    主要是回调onTouch和onTouchEvent方法,返回值取决于onTouch和onTouchEvent的返回值:

    • onTouch为true,会屏蔽onTouchEvent使其不再被调用,dispatchTouchEvent最终返回值为true,
    • onTouch为false,onTouchEvent也会被调用,且dispatchTouchEvent最终的返回值 = onTouchEvent的返回值。
  2. 如果方法入参child非空,就调用child.dispatchTouchEvent。由于这个方法就是dispatchTouchEvent调用的,因此这里child.dispatchTouchEvent就形成了递归调用,形成递归调用child的dispatchTouchEvent。

  3. dispatchTransformedTouchEvent的返回值就是dispatchTouchEvent的返回值。

addTouchTarget:

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

生成一个TouchTarget持有child子控件,把这个TouchTarget赋给mFirstTouchTarget。即生成mFirstTouchTarget持有child子控件。

dispatchTouchEvent控制事件分发

通常事件分发是在down事件时被决定,然后再以down事件决定的分发策略分发后续的move事件,那么我们就把down事件和move事件分开,分别分析。

down事件

(已做删减的伪代码,只剩下主体流程相关代码,方便讲解)

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    TouchTarget newTouchTarget = null;
    if (!canceled && !intercepted) { // true
        if (是按下事件?) {
            // 0 有子控件进入
            if (有子控件?) {
                for (遍历所有直接子控件,从远到近遍历) {
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
                    // 1 递归调用子控件dispatchTouchEvent
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 2 如果子控件返回true,进入,表示子控件愿意处理
                        // addTouchTarget封装保存子控件到 mFirstTouchTarget
                        // mFirstTouchTarget.child表示子控件
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        // 标记
                        alreadyDispatchedToNewTouchTarget = true;
                        break; // 取到一个合适子控件就跳出,表示只取一个有效子控件。针对控件由多个子控件的场景。
                    }
                }
            }
        }
    }
 
    if (mFirstTouchTarget == null /*表示子控件都不愿处理*/) {
        // 3
        // 在步骤1调用子控件时子控件返回了false,会走进这里。
        // 表示子控件不处理,这时就要检查自己是否处理,方式就是调用自己的super.dispatchTouchEvent
        // super.dispatchTouchEvent回调自己的onTouch系列回调,取其返回值
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // 3 如果有子控件处理,mFirstTouchTarget就非空进入这里
        TouchTarget target = mFirstTouchTarget;
        while (target != null) { // 通常情况mFirstTouchTarget链表只有一个子控件的值(即.next是空的),这里只会遍历一次就跳出
            final TouchTarget next = target.next;
            // 4 在2步骤做的标记,表示Down动作已经做过遍历dispatch处理,直接做返回标记handled即可
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                // 会进入这里,确定返回值为true。
                handled = true;
            } else {
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            target = next;
        }
    }
    // 5
    return handled;
}

结合代码注释分析说明

  1. 按下事件且有子控件的控件,会遍历所有子控件并调用dispatchTransformedTouchEvent(..., child, ...),前面有提过,这里实际是调用子控件的dispatchTouchEvent。子控件dispatchTouchEvent也会走到这里,于是触发递归调用子控件dispatchTouchEvent,一直递归到最外层子控件。

  2. 步骤1调用子控件dispatchTouchEvent,暂时抛开递归,看子控件dispatchTouchEvent返回值

    • 子控件.dispatchTouchEvent返回true,进入if分支执行addTouchTarget,生成了持有子控件的mFirstTouchTarget ,标记 alreadyDispatchedToNewTouchTarget = true
    • 子控件.dispatchTouchEvent返回false,不进入if分支,则 mFirstTouchTarget = null ,标记alreadyDispatchedToNewTouchTarget = false
  3. mFirstTouchTarget == null的判断,从步骤2可以看出,子控件返回false的控件的mFirstTouchTarget为null

    • mFirstTouchTarget == null分支:调用了dispatchTransformedTouchEvent(, null, )。之前有讲过,dispatchTransformedTouchEvent入参child为null时会调用本控件的super.dispatchTouchEvent,即调用view.dispatchTouchEvent,实际就是调用本控件的onTouch系列方法,并返回其返回值。
    • mFirstTouchTarget != null分支:会遍历mFirstTouchTarget的next,通常mFirstTouchTarget是空的,本文就以空为准讲解,即操作mFirstTouchTarget。这里判断alreadyDispatchedToNewTouchTarget为true且target == newTouchTarget(不相等的情况出现在mFirstTouchTarget存在next的情况,本文不讨论)成立,返回值handled设置为true。

具体场景演示

下面我们设置一个具体场景,走一遍完整过程,看一下发生了什么:

场景: 根布局控件A持有子控件B,B持有子控件C,C持有子控件D,假设4个控件都重写了dispatchTouchEvent,返回值分别设置为A:true B:true C:false D:true,触摸发生在控件D的区域。

1 控件A的dispatchTouchEvent被调用,进入到代码段1,调用dispatchTransformedTouchEvent(控件B),控件A的dispatchTouchEvent进入到代码段1,调用dispatchTransformedTouchEvent(控件C)...一直递归调用到控件D;

2 D没有子控件不会执行代码段1直接进入代码段3,由于没有执行代码段1、2,代码段3进入mFirstTouchTarget == null分支,执行dispatchTransformedTouchEvent(null),即执行控件D的onTouch系列方法,并返回其返回值,控件D的dispatchTouchEvent执行完毕,返回true

3 回到控件C的代码段1,由于子控件D返回了true,进入if分支执行代码段2,控件C的mFirstTouchTarget被赋值,持有子控件D,alreadyDispatchedToNewTouchTarget = true

4 控件C进入代码段3,mFirstTouchTarget非空进入else分支。根据前面对代码的分析我们知道,这里就执行了handled = true作为返回值。控件C的dispatchTouchEvent执行完毕,返回false(场景预设了方法被重写强制设置了false返回值)。

5 回到控件B的代码段1,由于子控件C返回了false,B不进入if分支不执行代码段2,进入到代码段3进入mFirstTouchTarget == null分支,执行dispatchTransformedTouchEvent(null)实际执行了控件B.super.dispatchTouchEvent,即执行控件B的ontouch系列方法,并返回其返回值。控件B的dispatchTouchEvent执行完毕,场景预设了方法被重写强制设置了返回值为true。

6 回到控件A的代码段1,由于子控件B返回了true,进入if分支执行代码段2,控件A的mFirstTouchTarget被赋值,持有子控件B,alreadyDispatchedToNewTouchTarget = true

7 控件A进入代码段3,mFirstTouchTarget非空进入else分支。进入(alreadyDispatchedToNewTouchTarget && target == newTouchTarget)分支执行handled = true作为返回值。控件A的dispatchTouchEvent执行完毕,场景预设了方法被重写强制设置了返回值为true。

汇总整个过程:

  • 各控件的mFirstTouchTarget情况:控件D:mFirstTouchTarget == null,控件C:mFirstTouchTarget被赋值持有子控件D,控件B:mFirstTouchTarget == null,控件A:mFirstTouchTarget被赋值持有子控件B。

  • 各控件的回调方法的执行情况:所有的控件的dispatchTouchEvent都被执行了,控件B和控件D的onTouch系列方法被执行了。

move事件

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    TouchTarget newTouchTarget = null;
    if (!canceled && !intercepted) { // true
        // 0 不是按下事件,这里什么也不做
        if (按下事件?) {
            ...
        }
    }
    // 1
    if (mFirstTouchTarget == null /*表示子控件都不愿处理*/) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // mFirstTouchTarget非空进入
        TouchTarget target = mFirstTouchTarget;
        while (target != null) { // 通常情况mFirstTouchTarget链表只有一个子控件的值,这里只遍历一次就跳出了
            final TouchTarget next = target.next;
            // 2 不是按下事件,没有代码段0中alreadyDispatchedToNewTouchTarget赋值等的处理,必进入else
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                // 3 递归调用子控件dispatchTouchEvent,获得返回值标记给handled
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            target = next;
        }
    }

    return handled;
}

结合代码注释分析说明

  1. 与down事件不同,之前down事件的代码段1、2只提供给down事件,move事件不会执行。

  2. 之前down事件已经确定了mFirstTouchTarget的值,进入代码段1判断mFirstTouchTarget

    • mFirstTouchTarget == null分支,执行dispatchTransformedTouchEvent(null),实际就是执行本控件的onTouch系列方法
    • mFirstTouchTarget != null分支,进入代码段2,由于没有前面的down事件时的代码段1、2的过程,alreadyDispatchedToNewTouchTarget = false,因此代码段2在move事件下永远为false,进入else分支
  3. 进入代码段3,调用dispatchTransformedTouchEvent(mFirstTouchTarget.child),形成递归调用子控件的dispatchTouchEvent方法。这个递归最终会停止在mFirstTouchTarget为空的控件,此控件的调用就变成了dispatchTransformedTouchEvent(null),即调用super.dispatchTouchEvent的情况,实际调用该控件的onTouch系列方法,达成了该控件消费事件的效果。

结合上文down事件的情况,可以发现,mFirstTouchTarget为null的情况发生在控件没有子控件或子控件返回false的场景,再来看move事件的递归,是从根布局控件开始向子控件方向通过mFirstTouchTarget递归子控件。这样的话move的递归会停止在从根布局向子布局的方向首个mFirstTouchTarget为null的控件,同时也是首个dispatchTouchEvent返回false的控件的父控件。

具体场景演示

继续以down事件举例的场景为例讲解

场景: 根布局控件A持有子控件B,B持有子控件C,C持有子控件D,假设4个控件都重写了dispatchTouchEvent,返回值分别设置为 A:true B:true C:false D:true,触摸发生在控件D的区域。

down事件讲解时已经总结了各控件的mFirstTouchTarget情况:控件D:mFirstTouchTarget == null,控件C:mFirstTouchTarget被赋值持有子控件D,控件B:mFirstTouchTarget == null,控件A:mFirstTouchTarget被赋值持有子控件B。

  1. 从根布局A开始进入dispatchTouchEvent方法,A进入代码段1判断mFirstTouchTarget != null 进入else分支,进入代码段3,执行dispatchTransformedTouchEvent(控件B)
  2. 控件B进入dispatchTouchEvent,进入代码段1判断mFirstTouchTarget == null,进入if分支,执行dispatchTransformedTouchEvent(null),即执行自己的onTouch系列方法,控件B的dispatchTouchEvent执行结束
  3. 回到布局A,子控件执行完毕,子控件的返回值只会影响控件A的返回值,没有其他内容了,执行完毕。

从上面我们能看出,影响是否执行的条件是mFirstTouchTarget是否有值,此时dispatchTouchEvent的返回值已经不能再影响事件的分配了。

结果:A、B控件的dispatchTouchEvent被执行了,B控件的onTouch系列方法被执行了(仿佛从down事件dispatchTouchEvent返回值为false的C控件处截断了)。最终效果就是B控件消费了事件。

总结

down事件中的关键代码:是从根布局控件向外层子控件的方向递归调用dispatchTouchEvent方法,对于返回值是false的控件,他的父控件就不会进入if分支去生成mFirstTouchTarget记录它自己的有效子控件。

move事件(消费)关键代码:根据 mFirstTouchTarget 寻找子控件,如果mFirstTouchTarget有值就调用其中的子控件的dispatchTouchEvent再去判断子控件的mFirstTouchTarget。

这样就可以看出,down事件负责组织mFirstTouchTarget形成一条链,move事件根据down事件组织的这条链去寻找链条末尾的控件,就是要消费事件的控件。

onTouch消费事件

上文通过代码分析+举例的方式进行了讲解,是建立在重写dispatchTouchEvent强制设置其返回值的前提下进行分析的。体现的是dispatchTouchEvent对事件分发的影响。以下我们讲onTouch是如何消费的。

首先我们确定一点,dispatchTouchEvent的优先级是高于onTouch的。在代码分析的最前面有写onTouch系列方法,是在View.dispatchTouchEvent中被调用的,他只能通过影响dispatchTouchEvent的返回值起作用,如果重写dispatchTouchEvent并设定其返回值,onTouch的返回值就完全没有作用了。

那么onTouch能够起作用的前提就是使用者不能影响dispatchTouchEvent的返回值,也就是即使重写了dispatchTouchEvent也要使用它原本的返回值。

down事件

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    TouchTarget newTouchTarget = null;
    if (!canceled && !intercepted) { // true
        if (是按下事件?) {
            // 0 有子控件进入
            if (有子控件?) {
                for (遍历所有直接子控件,从远到近遍历) {
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
                    // 1 递归调用子控件dispatchTouchEvent
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 2 如果子控件返回true,进入,表示子控件愿意处理
                        // addTouchTarget封装保存子控件到 mFirstTouchTarget
                        // mFirstTouchTarget.child表示子控件
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        // 标记
                        alreadyDispatchedToNewTouchTarget = true;
                        break; // 取到一个合适子控件就跳出,表示只取一个有效子控件。针对控件由多个子控件的场景。
                    }
                }
            }
        }
    }
    // 3
    if (mFirstTouchTarget == null /*表示子控件都不愿处理*/) {
        // 在步骤1调用子控件时子控件返回了false,会走进这里。
        // 表示子控件不处理,这时就要检查自己是否处理,方式就是调用自己的super.dispatchTouchEvent
        // super.dispatchTouchEvent回调自己的onTouch系列回调,取其返回值
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // 如果有子控件处理,mFirstTouchTarget就非空进入这里
        TouchTarget target = mFirstTouchTarget;
        while (target != null) { // 通常情况mFirstTouchTarget链表只有一个子控件的值(即.next是空的,有next的情况不在本文讨论范围,不考虑),这里只会遍历一次就跳出
            final TouchTarget next = target.next;
            // 在代码段2做的标记,if必定成立(target != newTouchTarget出现在mFirstTouchTarget存在next的场景,不在本文讨论范围)
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                // 4 设置返回值为true。
                handled = true;
            } else {
                ...
            }
            target = next;
        }
    }
    // 5
    return handled;
}

前面已经详细讲解过代码了,这里就直接以具体场景为例讲解了。

场景: 根布局控件A持有子控件B,B持有子控件C,C持有子控件D,D持有子控件E,E持有子控件F,假设6个控件都没有重写dispatchTouchEvent,但都重写了onTouch方法,返回值分别设置为 A:true B:true C:false D:true E:false F:false,触摸发生在控件F的区域。

  1. 首先从控件A开始进入代码段1开始递归调用子控件的dispatchTouchEvent直到最外层控件F,F没有子控件跳过代码段1、2进入到代码段3,由于没有执行代码段2,mFirstTouchTarget == null进入if分支,执行dispatchTransformedTouchEvent(null),即执行自己的onTouch方法,返回值为false,handled = false,dispatchTouchEvent执行完毕return handled false。
  2. F执行完毕跳出,来到控件E的代码段1,得到F的返回值false,跳过代码段2进入代码段3,同样由于没有执行2,mFirstTouchTarget == null进入if分支,执行dispatchTransformedTouchEvent(null),即执行自己的onTouch方法,返回值为false,handled = false,dispatchTouchEvent执行完毕return handled false。
  3. E执行完毕跳出,来到控件D的代码段1,得到E的返回值false,跳过代码段2进入代码段3,同样由于没有执行2,mFirstTouchTarget == null进入if分支,执行dispatchTransformedTouchEvent(null),即执行自己的onTouch方法,返回值为true,handled = true,dispatchTouchEvent执行完毕return handled true。
  4. D执行完毕跳出,来到控件C的代码段1,得到D的返回值true,进入代码段2,mFirstTouchTarget被创建并持有child为D,来到代码段3进入else分支,else分支直接进入代码段4,handled = true,最终return handled true。
  5. C执行完毕跳出,来到控件B的代码段1,得到C的返回值true,进入代码段2,mFirstTouchTarget被创建并持有child为C,来到代码段3进入else分支,else分支直接进入代码段4,handled = true,最终return handled true。
  6. B执行完毕跳出,来到控件A的代码段1,得到B的返回值true,进入代码段2,mFirstTouchTarget被创建并持有child为B,来到代码段3进入else分支,else分支直接进入代码段4,handled = true,最终return handled true。

执行结果:

  • 所有控件的dispatchTouchEvent被执行,控件F E D的onTouch方法被执行。
  • 控件A:mFirstTouchTarget.child->B,控件B:mFirstTouchTarget.child->C,控件C:mFirstTouchTarget.child->D,控件D E F:mFirstTouchTarget = null

从down的代码分析看,代码段1是从根向叶子的递归调用,那么返回肯定就是从最叶子节点开始返回的。每一个子控件的返回决定了父控件是否能进入代码段2进行mFirstTouchTarget的创建。而一旦出现返回true的子控件,父控件就获得了创建mFirstTouchTarget的机会并进入代码段3的判定else进入代码段4,这里的设计代码段4为handled = true,而我们设定的场景是不干预dispatchTouchEvent的返回值,这就决定了,即使该控件的onTouch返回false,dispatchTouchEvent也规定为handled = true返回true。同样的,自己返回true父控件也会经历自己同样的过程,也会进入代码段4强制设置为true而忽略onTouch的返回值,并且压根就没有机会调用到onTouch。

根据上面的情况我们发现,down事件组织出的mFirstTouchTarget链,是从最外层叶子开始向根部方向,寻找出第一个onTouch返回true的控件,一旦出现,那么从根布局到该控件就能形成一条链而这一条链中出现onTouch返回false也不会再起作用,因为从这个控件向根布局继续跳出递归的过程已经不会再进入代码段3的mFirstTouchTarget == null的分支了,也就没有机会再执行onTouch方法了。

move事件

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    TouchTarget newTouchTarget = null;
    if (!canceled && !intercepted) { // true
        // 0 不是按下事件,这里什么也不做
        if (按下事件?) {
            ...
        }
    }
    // 1
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else { // mFirstTouchTarget非空进入
        TouchTarget target = mFirstTouchTarget;
        while (target != null) { // 通常情况mFirstTouchTarget链表只有一个子控件的值,这里只遍历一次就跳出了
            final TouchTarget next = target.next;
            // 不是按下事件,alreadyDispatchedToNewTouchTarget赋值等处理,必进入else分支
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                ...
            } else {
                // 2 递归调用子控件dispatchTouchEvent
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            target = next;
        }
    }

    return handled;
}

场景: 根布局控件A持有子控件B,B持有子控件C,C持有子控件D,D持有子控件E,E持有子控件F,假设6个控件都没有重写dispatchTouchEvent,但都重写了onTouch方法,返回值分别设置为 A:true B:true C:false D:true E:false F:false,触摸发生在控件F的区域。

down事件的结果:控件A:mFirstTouchTarget.child->B,控件B:mFirstTouchTarget.child->C,控件C:mFirstTouchTarget.child->D,控件D E F:mFirstTouchTarget = null

  1. 控件A进入dispatchTouchEvent,进入代码段1,mFirstTouchTarget非空进入else分支,执行代码段2,dispatchTransformedTouchEvent(控件B)
  2. 控件B进入dispatchTouchEvent,进入代码段1,mFirstTouchTarget非空进入else分支,执行代码段2,dispatchTransformedTouchEvent(控件C)
  3. 控件C进入dispatchTouchEvent,进入代码段1,mFirstTouchTarget非空进入else分支,执行代码段2,dispatchTransformedTouchEvent(控件D)
  4. 控件D进入dispatchTouchEvent,进入代码段1,mFirstTouchTarget == null进入if分支,执行dispatchTransformedTouchEvent(null),即执行自己的onTouch方法,返回值赋给handled成为dispatchTouchEvent的返回值
  5. 控件D执行完毕,回到控件C,根据D的返回值确定handled的值,最终确定控件C的dispatchTouchEvent的返回值。
  6. 控件C执行完毕,回到控件B,根据C的返回值确定handled的值,最终确定控件B的dispatchTouchEvent的返回值。
  7. 控件B执行完毕,回到控件A,根据B的返回值确定handled的值,最终确定控件A的dispatchTouchEvent的返回值。

可以看到,只有控件D的onTouch方法被执行了,A B C D的dispatchTouchEvent被执行。

另外可以看出,返回值只会影响父控件的返回值,其他没有影响。

从效果看,move事件只给消费事件的控件D自己消费。

从move的代码分析看,只有mFirstTouchTarget == null的控件有机会执行onTouch方法,而down事件确定的链表中,肯定只有链条最末尾的控件的mFirstTouchTarget为空。

总结:

总结down事件和move事件的结论可以得出:在不干预dispatchTouchEvent返回值的情况下,onTouch系列方法的消费规则是:down事件分发时,从最外层叶子到根布局的方向,出现第一个onTouch返回true的控件,就是消费事件的控件。只有这个控件的onTouch可以收到后续的move事件。

事件拦截和申请取消拦截

事件拦截主要涉及 onInterceptTouchEvent 方法和 requestDisallowInterceptTouchEvent,onInterceptTouchEvent是拦截事件处理的,而requestDisallowInterceptTouchEvent是申请取消父控件的拦截事件的。

申请取消拦截

先从简单的入手,看requestDisallowInterceptTouchEvent如何申请取消拦截。

Java 复制代码
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

申请取消拦截的requestDisallowInterceptTouchEvent方法,disallowIntercept入参为true表示申请不拦截,实际就是给mGroupFlags加上标记 FLAG_DISALLOW_INTERCEPT

Java 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (onFilterTouchEventForSecurity(ev)) {
        ...

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            // disallowIntercept true申请取消拦截,进入else
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                // 强制设置为false,不拦截
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

在dispatchTouchEvent代码中,根据FLAG_DISALLOW_INTERCEPT标志对disallowIntercept赋值,为true表示申请不拦截,可以看到代码中走else分支把intercepted = false,跳过了onInterceptTouchEvent对拦截的赋值。

拦截

再回来看重头戏拦截:

down事件:

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
        ...
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            // ACTION_DOWN 表示一组事件的开始,所有状态初始化(包括FLAG_DISALLOW_INTERCEPT 阻止父类拦截的状态也被清除)
            resetTouchState();
        }
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            // 0 子控件阻止父控件拦截的标志
            if (!disallowIntercept) {
                // 1 回调拦截,返回值赋值 intercepted
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        // 2
        if (!canceled && !intercepted) {
            if (按下事件?) {
                // 有子控件进入
                if (有子控件?) {
                    for (遍历所有直接子控件,从远到近遍历) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        // 递归调用子控件dispatchTouchEvent
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 如果返回true进入,表示子控件愿意处理
                            // 封装保存子控件到 mFirstTouchTarget(addTouchTarget方法做的)和做标记
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break; // 取到一个合适子控件就跳出,只取一个
                        }
                    }
                }
            }
        }
        // 3
        if (mFirstTouchTarget == null) {
            // 没有子控件可处理,自己处理
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 有子控件处理,给父控件返回true
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    ...
                }
                predecessor = target;
                target = next;
            }
        }
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            // 一组事件结束,重置所有状态(包括FLAG_DISALLOW_INTERCEPT 阻止父类拦截的状态)
            resetTouchState();
        }
    return handled;
}

onInterceptTouchEvent返回true表示拦截,赋值给intercepted = true,在代码段2处有判断,intercepted为true就不走进if分支,该if分支就是前面我们分析事件分发时,down事件递归处理的逻辑处,不执行就代表,自本控件起,后续子控件连down事件都分配不到。对于onInterceptTouchEvent返回true的本控件,不执行开始的递归操作,mFirstTouchTarget就不会赋值,进入代码段3 mFirstTouchTarget == null直接执行自己的onTouch方法,返回值决定dispatchTouchEvent的返回值。

至此,拦截相关动作介绍已结束,后续的逻辑和前面讲事件分发就都一样了。

我们可以理解为,执行onInterceptTouchEvent返回true的控件,把自己后续的控件截断了,等同于自己变成是最外侧叶子结点,再没有子控件了。

我们上面讲解的,是在down事件就做拦截的。前面的事件分发讲解时我们已经知道,down事件决定了后续事件分发,如果我们在down事件时就拦截,被拦截的后续子控件不光得不到后续的move事件,就连down事件也没有被分配到。这就导致被拦截的子控件仿佛就没有存在过,根本就没有机会调用requestDisallowInterceptTouchEvent去申请取消父控件的拦截。

那么,如果我们设计的布局,想要允许子控件根据业务需要申请取消父控件的拦截,那就需要放过down事件,在move事件中做拦截。

我们继续来看move事件如何做拦截的。

move事件:

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 0 子控件申请阻止父控件拦截的的标记
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    // 1
    if (!canceled && !intercepted) {
        if (按下事件?) {
            ...
        }
    }
    // 2
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // mFirstTouchTarget非空进入
        TouchTarget target = mFirstTouchTarget;
        while (target != null) { // 通常情况mFirstTouchTarget链表只有一个子控件的值,这里只遍历一次就跳出了
            final TouchTarget next = target.next;
            // 不是按下事件,没有上面的处理,进入else,表示非按下事件,还没处理过dispatch,走else做处理
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                // 3
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                // 递归调用子控件dispatchTouchEvent,获得返回值标记给handled
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            target = next;
        }
    }
    if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        resetTouchState();
    }
    return handled;
}

拦截事件即intercepted = true,在move事件时,代码段1本身也没有move相关逻辑,直接到代码段2,走else分支(if分支本身就是没有子view的处理分支)来到代码段3,intercepted把cancelChild设置为true,进入dispatchTransformedTouchEvent准备开始递归

Java 复制代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    final int oldAction = event.getAction();
    // 1
    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;
    }

    ...
    // 2
    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);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

cancelChild = true对应的入参cancel,代码段2是正常逻辑,根据是否有child执行对应的dispatchTouchEvent形成递归调用子控件,而入参cancel = true就进入了代码段1的if分支,给子view设置ACTION_CANCEL事件并返回。

Java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        ...
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...

        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        ...
        // 1
        if (!canceled && !intercepted) {
            if (按下事件?) {
                ...

我们再看子view进入dispatchTouchEvent的代码,ACTION_CANCEL将canceled设为true,来到代码段1处的判断,和onInterceptTouchEvent起到的作用一样。

最后注意:1 想要允许子控件根据业务需要申请取消父控件的拦截,就把onInterceptTouchEvent拦截放在move事件时返回true。2 申请取消拦截是调用父控件的方法:getParent().requestDisallowInterceptTouchEvent(true)

相关推荐
ch_s_t18 分钟前
电子商务网站之首页设计
android
豆 腐3 小时前
MySQL【四】
android·数据库·笔记·mysql
想取一个与众不同的名字好难5 小时前
android studio导入OpenCv并改造成.kts版本
android·ide·android studio
Jewel1055 小时前
Flutter代码混淆
android·flutter·ios
Yawesh_best6 小时前
MySQL(5)【数据类型 —— 字符串类型】
android·mysql·adb
曾经的三心草9 小时前
Mysql之约束与事件
android·数据库·mysql·事件·约束
guoruijun_2012_413 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood13 小时前
一文了解Android中的AudioFlinger
android·音频
B.-15 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
有趣的杰克15 小时前
Flutter【04】高性能表单架构设计
android·flutter·dart