美文网首页
Touch 事件分发相关点

Touch 事件分发相关点

作者: 你可记得叫安可 | 来源:发表于2020-08-30 17:26 被阅读0次

概述

  • Touch 事件的分发是从上到下,从父到子:Activity -> ViewGroup1 -> ViewGroup1 的子 ViewGroup2 -> ... -> TargetView
  • Touch 事件的响应是从下到上,从子到父:TargetView -> ... -> ViewGroup1 的子 ViewGroup2 -> ViewGroup1 -> Activity
boolean dispatchTouchEvent(MotionEvent ev)

事件分发的处理函数,事件来自 Choreographer,开始分发的地方是 ActivityActivity 根据 UI 显示的情况,把事件传递给相应的 ViewGroup。该方法返回 true 表示事件不再往下分发,由于没有找到目标 View,因此也就表示将该事件丢弃

开发者通常不需要重写该方法。除非是自定义 ViewGroup,需要自定义分发顺序

boolean onInterceptTouchEvent(MotionEvent ev)

事件的拦截函数,这个是暴露给开发者根据自身需求需要重写的方法,返回 true 表示拦截。对不同事件的拦截,行为上有差异的表现:

  • 如果 ACTION_DOWN 事件没有被拦截,顺利找到了 TargetView,那么后续的 MOVEUP 都能经由该函数下发。如果后续的 MOVEUP 下发时还有继续拦截的话,事件就只能传递到拦截层,并且向 TargetView 发出 ACTION_CANCEL
  • 如果 DOWN 事件下发时就被拦截,且拦截层没有消费事件,导致没有找到 TargetView,那么后续的 MOVEUP 都无法向下派发了,在 Activity 层就终止。
boolean onTouchEvent(MotionEvent ev)

响应处理函数,如果有设置对应 listener 的话,这里还会与 onTouchonClickonLongClick 有关。执行顺序是 onTouch -> onTouchEvent -> onClick -> onLongClick。返回 true 表示被处理。该函数返回 true 表示事件被消费

requestDisallowInterceptTouchEvent 在 dispatchTouchEvent 中的应用

有时候我们不希望父 View 拦截事件,比如出现滑动冲突的时候,安卓系统给我们提供了 requestDisallowInterceptTouchEvent() 方法来通知父 View 不要拦截事件。使用这个方法的正确姿势,网上都是这样写的:

public boolean onTouchEvent(MotionEventevent){
    switch(event.getAction()){
        // 这里是解决滑动冲突的例子,一般来说滑动冲突都是因为父 View
        // 在收到滑动一定距离后,就在 onInterceptTouchEvent 中将 MOVE
        // 事件也给拦截掉了。因此才导致子 View 就滑动不了了。所以这里在
        // 子 View 还能收到 MOVE 事件时,就通知父 View 不要拦截。
        // 其实确定希望父 View 永远不要拦截的话,那么在 DOWN 中
        // requestDisallowInterceptTouchEvent() 也可以。
        case MotionEvent.ACTION_MOVE:
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            parent.requestDisallowInterceptTouchEvent(false);
            break;
    }
}

那么它是如何让父 View 不再拦截事件的呢?我们来看下 dispatchTouchEvent() 中涉及这部分的源码。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        resetTouchState();
    }
    
    // 下面判断是否符合调用 onInterceptTouchEvent() 的条件
    final boolean intercepted;
    // 当前是 DOWN 事件,或者已经有目标 View
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        // 这里 DOWN 事件时,disallowIntercept 一定为 false。
        // 因此 DOWN 事件时,onInterceptTouchEvent() 一定会被调
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            // 既不是 DOWN 事件,又没有目标 View 时(什么时候会出现这种情况?)
            intercepted = true;
        }
    }
    ...
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}
  • 当父 View 收到 DOWN 事件时,会首先调用 resetTouchState() 方法将 FLAG_DISALLOW_INTERCEPT 标志位清零,这就是为什么子 View 如果在构造方法中调用 requestDisallowInterceptTouchEvent() 没有用的原因,子 View 最早都要在收到 DOWN 事件后调用 requestDisallowInterceptTouchEvent() 才会起作用。
  • 当第一次的 DOWN 事件下发时,父 View 一定会调用一次 onInterceptTouchEvent()。如果这时父 View 进行了拦截,那么子 View 就永远也收不到任何事件,也就没有机会调用 requestDisallowInterceptTouchEvent()

事件如何由父 View 分发给子 View 的

我们还是看父 View.dispatchTouchEvent()。总体上,父 View 是以广度遍历的形式来将事件分发给各个子 View 的:

// 不是 CANCEL 并且不拦截
if (!canceled && !intercepted) {
    // 在 DOWN 时去寻找目标 view
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
       final int childrenCount = mChildrenCount;
       if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            ...
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); // 这里就是从 childView 数组中取得一个 View,只不过综合了一个自定义 Child 排序,这里先不管
                // 如果坐标不在 child 的范围内,则跳过该 child
                if (!child.canReceivePointerEvents() || !isTransforedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
                
                newTouchTarget = getTouchTarget(child);
                ...
                // 这里将 DOWN 事件传给 child(该调用在 DOWN 事件的条件分支里),调用 child 的 dispatchTouchEvent() 方法去分发
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    mLastTouchDownTime = ev.getDownTime();
                    mLastTouchDownX = ev.getX();
                    mLastTouchDownY = ev.getY();
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
       }
    }
    
    // 经过上面的逻辑后,只存在下面两种可能:
    // 1. 没有找到目标 view。那么该 ViewGroup 就看做普通 View:直接调用 super.dispatchTouchEvent(),向上回调
    // 2. 找到了目标 view,那么后续事件都可以直接到目标 view
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); // 注意这里第三个参数是 child,传的是 null,这会导致方法中直接调用 super.dispatchTouchEvent()
    } else {
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            // 如果前面 DOWN 找到目标 view 时已经消费掉 DOWN 事件,那么这里直接返回 true
            handled = true;
        } else {
            // 如果不是 DOWN 事件,又被拦截了,则需要向之前的事件传递链上的 child 发送 CANCEL 事件
            final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; 
            // 把事件传递给目标 view 或者 传递 CANCEL 事件给目标 view
            if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                handled = true;
            }
        }
    }
    
    if (canceled || actionMasked == MotionEvent.ACTION_UP) {
        // 如果是 CANCEL 或者 UP,重置 Touch 状态标识,mFirstTouchTarget 赋值为 null,后续的事件无法派发给 子 View 了。
        resetTouchState();
    }
}

子 View 在哪里消费事件,以及如何通知上层父 View 的?

我们再看上面的代码,在广度遍历各个子 View 时,调用了 dispatchTransformedTouchEvent() 方法:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    final boolean handled;
    
    // 处理 CANCEL
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) { // 前面事件分发时,没有找到目标 view,则调用 super.dispatchTouchEvent(),由于本身是 ViewGroup,所以 super 就是 View,也就是将这个 ViewGroup 当成普通 View 来处理。
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
    // 普通事件,跟上面处理 CANCEL 一样,只不过 CANCEL 是往下派发 CANCEL。或者自己处理 CANCEL
    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);
    }
    
    return handled;
}

无论事件怎么派发,最终都是派发到了叶子 View 上,或者没有子 View 的话,通过 super.dispatchTouchEvent() 将自己当成一个 View(而不是 ViewGroup) 来处理。我们看 ViewdispatchTouchEvent():

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener) {
            result = true;
        }
    
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    ...
    return result;
}

从上面可以看到,叶子 ViewdispatchTouchEvent() 中就没有 OnInterceptTouchEvent() 了,取而代之的是 OnTouchListenerOnTouchEvent,而且如果 OnTouchListener 先消费事件的话,那么 onTouchEvent 就不会再调用。

我们再看 OnTouchEvent() 中做了什么,代码有点多,我们就捡几个重点说一下:

  • performClick() 是在 ACTION_UP 中进行的。
image.png

下面我们用 demo 来看看事件的具体分发,View 的层级关系依次是 MainActivity -> ParentLayout -> ChildLayout -> InnerButton

Case 0:正常下发事件

image.png

Case 1: DOWN 在任意层被抛弃

Case 1.1 抛弃后,后续事件不被拦截

在 ChildLayout 层被抛弃,且后序事件不被拦截
image.png
  • DOWNChildLayout 被抛弃
  • 之后事件走正常下发流程,而且只会分发到抛弃层,即 ChildLayout。并且将 ChildLayout 当做普通 View 来看待,即不会调用它的 onIntercept()。后续事件回传时,中间层的 onTouch() 也不会被调用,但是会调用 Activity.onTouch()
在 Activity 层被抛弃
image.png
  • 本质上跟上面是一样的。

Case 1.2: 抛弃后,后续事件被中间层拦截

image.png
  • DOWN 事件在哪一层被抛弃,系统就认为那一层是暂时的目标 View,因此后续事件会走正常流程下发到抛弃层
  • 后序事件下发过程中,中间层可拦截,由于有目标 View,因此中间层拦截后,会向下往目标 View 发送 CANCEL
    • 如果没有 View 消费 CNCEL,则 Activity.onTouch() 会收到 MOVE但是第一个拦截事件不会调用拦截层及其他中间层的 onTouch()
    • 如果有 View 消费 CANCEL,则往上回调时,不会有任何其他的 onTouch() 会被调用。
  • 拦截事件后的所有事件,都会将拦截层当做新的目标 View,事件下发到拦截层。
  • 说明被抛弃并不会改变目标 View,但是被拦截就会改变目标 View
  • 被抛弃并不会触发 CANCEL 事件的下发,但是拦截中间事件触发 CANCEL 发向目标 View
  • 一旦有 onTouch() 被调用,那么回溯时就不会再有 onTouch() 被调用。
  • 在任意层调用过 onTouch(),且事件未被消费,则 Activity.onTouch() 一定会被调用

Case 2: DOWN 被消费

Case 2.1:DOWN 被 InnerButton 消费,后序事件在 ChildLayout 层被抛弃
image.png
  • DOWNInnerButton 消费没什么好说的,走正常流程。
  • MOVEChildLayout 抛弃,被抛弃前走正常流程
  • 之后事件 及 UP 走正常流程

Case 2.2: DWON 被 InnerButton 消费,后续事件在 ChildLayout 层被拦截

image.png
  • DOWNInnerButton 消费没什么好说的,走正常流程
  • MOVEChildLayout 拦截,由于之前 DOWN 已经被消费,因此有目标 View,向目标 View 发送 CANCEL.
  • 之后的事件只下发到新的目标 ChildLayout

Case 3: DOWN 被中间层拦截

case 3.1: DOWN 被 ChildLayout 拦截,且被消费
image.png
  • 在还没有目标 View 时,拦截事件,会马上调用拦截层的 onTouch()。但是如果已经有目标 View 时,拦截事件,不会调用拦截层的 onTouch(),但是会向目标 View 发送 CANCEL 事件。后续事件才会将拦截层当做目标 View
case 3.2: DOWN 被 ChildLayout 拦截,且未被消费
image.png
  • DOWN 在下发时被 ChildLayout 拦截。由于此时还没有目标 View,因此马上调用拦截层的 onTouch()ChildLayout.onTouch() 也不消费事件,因此层层往上回溯 onTouch() 直到 Activity
  • 由于没有目标 View,因此之后的所有事件都只能到达 Activity

相关文章

  • Touch 事件分发相关点

    概述 Touch 事件的分发是从上到下,从父到子:Activity -> ViewGroup1 -> ViewGr...

  • Android事件分发

    Touch事件的相关方法 dispatchTouchEvent        事件分发onInterceptTo...

  • Android事件分发机制

    一.概述 事件分发有多种类型, 本文主要介绍Touch相关的事件分发. 整个事件分发流程中,会有大量MotionE...

  • Android事件分发与消费机制

    一、Touch 事件分析: 事件分发:dispatchTouchEvent return true:事件会分发给当...

  • 安卓事件分发

    事件分发 MotionEvent 当用户触摸屏幕时,将产生点击事件,Touch事件的相关细节(发生触摸的位置、时间...

  • Android 事件分发总结

    一、View事件分发 先看看这几种现象 View与Touch事件相关的两个方法 1.1 dispatchTouch...

  • 有关view的事件分发

    事件分发:用户点击屏幕(view或者viewGroup)产生点击事件(touch事件),touch事件的(发生触摸...

  • Android事件分发

    事件分发的对象是点击事件(touch事件) 当用户触摸手机屏幕的时候就会产生点击事件(Touch事件),touch...

  • 【Android】事件分发机制

    一、事件分发机制过程 Android事件分发机制是Android开发必须掌握的东西,分发的事件是点击Touch事件...

  • Touch分发的结论

    Touch传递 Touch事件先传递到Activity,然后ViewGroup,再传递到View。 Touch分发...

网友评论

      本文标题:Touch 事件分发相关点

      本文链接:https://www.haomeiwen.com/subject/mquesktx.html