View的事件分发及冲突处理

一 MotionEvent 事件

二 MotionEvent 事件分发  

当一个 MotionEvent 产生了以后,就是你的手指在屏幕上做一系列动作的时候,系统需要把这一系列的 MotionEvent 分发给一个具体的 View。我们重点需要了解这个分发的过程,那么系统是如何去判断这个事件要给哪个 View,也就是说是如何进行分发的呢?

事件分发需要 View 的三个重要方法来共同完成:

  • public boolean dispatchTouchEvent(MotionEvent event)
    事件分发的重要方法。那么很明显,如果一个 MotionEvent 传递给了 View,那么 dispatchTouchEvent 方法一定会被调用!
    返回值:表示是否消费了当前事件。可能是 View 本身的 onTouchEvent 方法消费,也可能是子 View 的 dispatchTouchEvent 方法中消费。返回 true 表示事件被消费,本次的事件终止。返回 false 表示 View 以及子 View 均没有消费事件,将调用父 View 的 onTouchEvent 方法
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    事件拦截,当一个 ViewGroup 在接到 MotionEvent 事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是 ViewGroup 特有的方法,View 并没有拦截方法
    返回值:是否拦截事件传递,返回 true 表示拦截了事件,那么事件将不再向下分发,而是调用 View 本身的 onTouchEvent 方法。返回 false 表示不做拦截,事件将向下分发到子 View 的 dispatchTouchEvent 方法
  • public boolean onTouchEvent(MotionEvent ev)
    真正对 MotionEvent 进行处理或者说消费的方法。在 dispatchTouchEvent 进行调用。
    返回值:返回 true 表示事件被消费,本次的事件终止。返回 false 表示事件没有被消费,将调用父 View 的 onTouchEvent 方法

上面的三个方法可以用以下的伪代码来表示其之间的关系。

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false; // 事件是否被消费
        
        if (onInterceptTouchEvent(ev)) { // 调用 onInterceptTouchEvent 判断是否拦截事件
            consume = onTouchEvent(ev);// 如果拦截则调用自身的 onTouchEvent 方法
        } else {
            // 不拦截调用子 View 的 dispatchTouchEvent 方法
            consume = child.dispatchTouchEvent(ev);
        }
        
        // 返回值表示事件是否被消费,true 事件终止,false 调用父 View 的 onTouchEvent 方法
        return consume;
    }

通过上面的介绍,相信我们已经初步了解了 View 的事件分发的机制。

接下来我们来看一下 View 和 ViewGroup 在事件分发的时候有什么不一样的地方:

ViewGroup 是 View 的子类,也就是说 ViewGroup 本身就是一个 View,但是它可以包含子 View(当然子 View 也可能是一个 ViewGroup),所以不难理解,上面所展示的伪代码表示的是 ViewGroup 处理事件分发的流程。而 View 本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在 onTouchEvent 方法中进行处理消费或者不消费。

上面结论先简单的理解一下,通过下面的流程图,会更加清晰的帮助我们梳理事件分发机制

可以看出事件的传递过程都是从父 View 到子 View。

但是这里有三点需要特别强调一下

  • 子 View 可以通过 requestDisallowInterceptTouchEvent 方法干预父 View 的事件分发过程(ACTION_DOWN 事件除外),而这就是我们处理滑动冲突常用的关键方法
  • 对于 View 而言,如果设置了onTouchListener,那么 OnTouchListener 方法中的 onTouch 方法会被回调。onTouch 方法返回 true,则 onTouchEvent 方法不会被调用(onClick 事件是在 onTouchEvent 中调用的)所以三者优先级是 onTouch -> onTouchEvent -> onClick

  • View 的 onTouchEvent 方法默认都会消费掉事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false),View 的 longClickable 默认为 false,clickable 需要区分情况,如 Button 的 clickable 默认为 true,而 TextView 的 clickable 默认为 false

三 View 事件分发源码

时序图:

我们知道当输入事件从 InputDispatcher 通过 server 端的 InputChannel 发送给应用程序的 client 端,其实最终事件被发送到了 java 层的 ViewRootImpl 中。

对于 java 层的事件分发从 ViewRootImpl.java 的 deliverInputEvent 开始,代码如下

private void deliverInputEvent(QueuedInputEvent q) {
        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
                q.mEvent.getSequenceNumber());
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
        }

        InputStage stage;
        if (q.shouldSendToSynthesizer()) {
            stage = mSyntheticInputStage;
        } else {
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }

        if (q.mEvent instanceof KeyEvent) {
            mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
        }

        if (stage != null) {
            handleWindowFocusChanged();
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

 ViewRootImpl 类定义了多种类型的 InputStage,以责任链模式进行处理分发输入事件

setView@ViewRootImpl.java
    // 接收事件
    --> mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper());
onInputEvent@ViewRootImpl.java#WindowInputEventReceiver.java
    --> enqueueInputEvent
        --> doProcessInputEvents();
            --> deliverInputEvent(q);
                --> stage.deliver(q); (InputStage stage;)
deliver@ViewPostImeInputStage.java
    --> onProcess(q);
        --> processPointerEvent
            // mView --> DecorView
            --> boolean handled = mView.dispatchPointerEvent(event);
                --> dispatchTouchEvent(event);

3.1 View.dispatchPointerEvent

@UnsupportedAppUsage
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }

 针对 Touch 事件又会调 dispatchTouchEvent 方法,此方法在 DecorView 中有重写:

 3.2 DecorView.dispatchTouchEvent

// DecorView.java 
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

这里的 Window.Callback 指向当前 Activity,在 Activity 启动时会调用自己的 attach 方法,此方法中会将自己作为 callback 传给 window

// Activity.java

final void attach(......){
    ......
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
	mWindow.setCallback(this); // 这里的 this 就是 Activity
	......
}

 所以 mWindow.getCallback() 获取的就是当前的 Activity,点击事件产生后,最先传递到当前的 Activity,由 Acivity 的 dispatchTouchEvent 方法来对事件进行分发。那么很明显我们先看 Activity 的 dispatchTouchEvent 方法:

3.3 Activity.dispatchTouchEvent

// Activity.java    
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) { // 事件分发并返回结果
            return true; // 事件被消费
        }
        return onTouchEvent(ev); // 没有 View 可以处理,调用 Activity onTouchEvent 方法
    }

通过上面的代码我们可以发现,事件会给 Activity 附属的 Window 进行分发。如果返回 true,那么事件被消费。如果返回 false 表示事件发下去却没有 View 可以进行处理,则最后 return Activity 自己的 onTouchEvent 方法。

跟进 getWindow().superDispatchTouchEvent(ev) 方法发现是 Window 类当中的一个抽象方法

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
Class Window:
// 抽象方法,需要看 PhoneWindow 的实现
public abstract boolean superDispatchTouchEvent(MotionEvent event);

我们知道,Window 的唯一实现类是 PhoneWindow。那么去看 PhoneWindow 对应的代码。

class PhoneWindow
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

PhoneWindow 又调用了 DecorView 的 superDispatchTouchEvent 方法。而这个 DecorView 就是 Window 的顶级 View,我们通过 setContentView 设置的 View 是它的子 View(Activity 的 setContentView,最终是调用 PhoneWindow 的 setContentView,有兴趣同学可以去阅读,这块不是我们讨论的重点)

到这里事件已经被传递到我们的顶级 View 中,一般是 ViewGroup。

那么接下来重点将放到 ViewGroup 的 dispatchTouchEvent 方法中。我们之前说过,事件到达 View 会调用 dispatchTouchEvent 方法,如果 View 是 ViewGroup 那么会先判断是否拦截该事件。

3.4 ViewGroup.dispatchTouchEvent

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
		.......
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            // 多指还是单指操作,只会执行一次 -- 第二根手指是ACTION_POINT_DOWN
            // 重置状态
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                // 清除所有targets, mFirstTouchTarget = null,重置了mGroupFlags的值
                resetTouchState();
            }

            // Check for interception.
            // 检测是否拦截 -- 父容器的权利 第一部分代码
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                 // requestDisallowInterceptTouchEvent会改变mGroupFlags的值
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // disallowIntercept 决定onInterceptTouchEvent 是否执行
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
			.......
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
         	......
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            // 在if中分发事件 第二块代码
            if (!canceled && !intercepted) {
  				.......
                // 第一根手指按下时,命中if
                if (actionMasked == MotionEvent.ACTION_DOWN
                        // 第二根或第n根手指按下,命中if
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        // 鼠标
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    .......
                    // 有多少个子孩子
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                         ......
                        // 将子view进行排序
                        //排序是根据Z值来排的。这个Z值一般用不到。所以如果不写Z值,那么就默认根据你写的顺序来排序。比如		
                        //FrameLayout包括ViewPager和TextView,那么ViewPager就排在TextView的前面,以此类推。越是往前的,
                        // 它就优 先接收事件。      	    		                   
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 倒序遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                  			......
                            // 是否能处理点击事件
                            if (!child.canReceivePointerEvents() // view可见或者animation不为空为ture
                                    // 点击区域是否在 child views上面
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            // 单指操作,为null,多指才不为空
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            // 询问 child是否处理事件,如果child处理,则命中if -- 递归
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                               	......
                                // 创建了newTouchTarget == mFirstTouchTarget
                                // 此时mFirstTouchTarget赋值了
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                // 后面会用到
                                alreadyDispatchedToNewTouchTarget = true;
                                // 退出循环,不再循环其他child
                                break;
                            }
							........
                        }
                        .......
                    }
						.......
                }
            }

            // Dispatch to touch targets.第三块代码
            // 没有child处理事件的时候
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 询问自己是否处理
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 有子view处理了事件
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                // while循环为单指操作时,只会执行一次
                while (target != null) {
                    // 单指操作 next = null
                    final TouchTarget next = target.next;
                    // if命中,直接返回handle,不作处理
                    // alreadyDispatchedToNewTouchTarget为局部变量,当move事件过来不再询问子view是否处理,
                    // alreadyDispatchedToNewTouchTarget重置为false
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        // intercepted为true时cancelChild为true
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;

                        // 询问 target.child(前面遍历时addTouchTarget保存的)
                        // down事件谁处理的,move事件也是谁处理,其他view没资格处理
                        // 如果父view拦截子view的话,第一个move由子view处理,事件为cancel,第二个move由父view处理
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        // 为true 取消child处理事件
                        if (cancelChild) {
                            if (predecessor == null) {
                                // mFirstTouchTarget置为null
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
		.....
        return handled;
    }

我们前面说过子 View 可以通过 requestDisallowInterceptTouchEvent 方法干预父 View 的事件分发过程(ACTION_DOWN 事件除外)

为什么 ACTION_DOWN 除外?通过上述代码我们不难发现。如果事件是 ACTION_DOWN,那么 ViewGroup 会重置 FLAG_DISALLOW_INTERCEPT 标志位,并且将 mFirstTouchTarget 设置为 null。对于 mFirstTouchTarget 我们可以先这么理解,如果事件由子 View 去处理时,mFirstTouchTarget 会被赋值并指向子 View。

所以当事件为 ACTION_DOWN 或者 mFirstTouchTarget != null(即事件由子 View 处理)时会进行拦截判断。具体规则是如果子 View 设置了 FLAG_DISALLOW_INTERCEPT 标志位,那么 intercepted =false。否则调用 onInterceptTouchEvent 方法。

如果事件不为 ACTION_DOWN 且事件为 ViewGroup 本身处理(即 mFirstTouchTarget ==null )那么 intercepted = true,很显然事件已经交给自己处理根本没必要再调用 onInterceptTouchEvent 去判断是否拦截。

结论:
当 ViewGroup 决定拦截事件后,后续事件将默认交给它处理并且不会再调用 onInterceptTouchEvent 方法来判断是否拦截。子 View 可以通过设置 FLAG_DISALLOW_INTERCEPT 标志位来不让 ViewGroup 拦截除 ACTION_DOWN 以外的事件。

我们知道 onInterceptTouchEvent 并非每次都会被调用。如果要处理所有的点击事件那么需要选择 dispatchTouchEvent 方法,而 FLAG_DISALLOW_INTERCEPT 标志位可以帮助我们去有效的处理滑动冲突。

如果 ViewGroup 不拦截事件,那么事件将下发给子 View 进行处理。

上面代码是将事件分发给子 View 的关键代码,需要关注的地方都加了注释。分发过程首先需要遍历 ViewGroup 的所有子 View,可以接收点击事件的 View 需要满足下面条件:

  • 如果 View 可见并且没有播放动画 canViewReceivePointerEvents 方法判断
    /**
     * Returns true if a child view can receive pointer events.
     * @hide
     */
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }
  • 点击事件的坐标落在 View 的范围内 isTransformedTouchPointInView 方法判断
    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        // 调用 View 的 pointInView 方法进行判断坐标点是否在 View 内
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

如果满足上面两个条件,接着我们看后面的代码 newTouchTarget = getTouchTarget(child);

    /**
     * Gets the touch target for specified child view.
     * Returns null if not found.
     */
    private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

可以看到当 mFirstTouchTarget 不为 null 的时候并且 target.child 就为我们当前遍历的 child 的时候,那么返回的 newTouchTarget 就不为 null,则跳出循环。

我们前面说过,当子 View 处理了点击事件那么 mFirstTouchTarget 就不为 nulll。事实上此时我们还没有将事件分发给子 View,所以正常情况下我们的 newTouchTarget 此时为 null

3.5 ViewGroup.dispatchTransformedTouchEvent

        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();
            // 当 child 处理了点击事件,那么会设置 mFirstTouchTarget 在 addTouchTarget 被赋值
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            // 子 View 处理了事件,然后就跳出了 for 循环
            break;
        }

 可以看到它被最后一个 if 包围,如果它返回为 true,那么就 break 跳出循环,如果返回为 false 则继续遍历下一个子 View。

我们跟进 dispatchTransformedTouchEvent 方法可以看到这样的关键逻辑:

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 先保存旧动作,然后因为cancle为true,所以进入if
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
            	// 子view收到cancel事件,是上层view拦截把事件置为ACTION_CANCEL触发
                handled = child.dispatchTouchEvent(event);
            }
            // 把事件重置为oldAction
            event.setAction(oldAction);
            return handled;
        }
		......
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
			.....
            // child是容器,ViewGroup.dispatchTouchEvent;child是view,view.dispatchTouchEvent(处理事件)
            handled = child.dispatchTouchEvent(transformedEvent);
        }
     	.....
        return handled;
    }

这里 child 是我们遍历传入的子 View 此时不为 null,则调用了 child.dispatchTouchEvent(event);

我们子 View 的 dispatchTouchEvent 方法返回 true,表示子 View 处理了事件,那么我们一直提到的,mFirstTouchTarget 会被赋值,是在哪里完成的呢?

再回头看 dispatchTransformedTouchEvent 则为 true 进入最后一个 if 语句,有这么一句 newTouchTarget = addTouchTarget(child, idBitsToAssign);

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

没错,mFirstTouchTarget 就是在 addTouchTarget 中被赋值!到此子 View 遍历结束。

如果在遍历完子 View 以后,ViewGroup 仍然没有找到事件处理者即 ViewGroup 并没有子 View 或者子 View 处理了事件,但是子 View 的 dispatchTouchEvent 返回了 false(一般是子 View 的 onTouchEvent 方法返回 false)那么 ViewGroup 会去处理这个事件。

从代码上看,就是我们遍历的 dispatchTransformedTouchEvent 方法返回了 false。那么 mFirstTouchTarget 必然为 null;

在 ViewGroup 的 dispatchTouchEvent 遍历完子 View 后,有下面的处理。

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        }

上面的 dispatchTransformedTouchEvent 方法第三个 child 参数传 null。

我们刚看了这个方法。当 child 为 null 时,handled = super.dispatchTouchEvent(event); 所以此时将调用 父View 的 dispatchTouchEvent 方法,点击事件给了 父View。到此事件分发过程全部结束!

结论:

  • ViewGroup 会遍历所有子 View 去寻找能够处理点击事件的子 View(可见,没有播放动画,点击事件坐标落在子 View 内部),最终调用子 View 的 dispatchTouchEvent 方法处理事件
  • 当子 View 处理了事件,则 mFirstTouchTarget 被赋值,并终止子 View 的遍历

  • 如果 ViewGroup 并没有子 View 或者子 View 处理了事件,但是子 View 的 dispatchTouchEvent 返回了 false(一般是子 View 的 onTouchEvent 方法返回 false)那么 ViewGroup 会去处理这个事件(本质调用 View 的 dispatchTouchEvent 去处理)

通过 ViewGroup 对事件的分发,我们知道事件最终是调用 View 的 dispatchTouchEvent 来处理。

View 最终是怎么去处理事件的

3.6 View.dispatchTouchEvent

// view.java    
public boolean dispatchTouchEvent(MotionEvent ev) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            // noinspection SimplifiableIfStatement
            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;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // 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.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

上面是 View 的 dispatchTouchEvent 方法的全部代码。相比 ViewGroup 我们需要好几段去拆开看的长篇大论而言,它就简洁多了。很明显 View 是单独的一个元素,它没有子 View,所以也没有分发的代码。我们需要关注的,也只是上面当中的一部分代码。

        // 如果窗口没有被遮盖
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            // noinspection SimplifiableIfStatement
            // 当前监听事件
            ListenerInfo li = mListenerInfo;
            // 需要特别注意这个判断当中的 li.mOnTouchListener.onTouch(this, event) 条件
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            // result 为 false 调用自己的 onTouchEvent 方法处理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

通过上面代码我们可以看到 View 会先判断是否设置了 OnTouchListener,如果设置了 OnTouchListener 并且 onTouch 方法返回了 true,那么 onTouchEvent 不会被调用。

当没有设置 OnTouchListener 或者设置了 OnTouchListener 但是 onTouch 方法返回 false,则会调用 View 自己的 onTouchEvent方 法。

接下来看 onTouchEvent 方法:

// view.java    

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        // 1.如果 View 是设置成不可用的(DISABLED)仍然会消费点击事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        ...
        // 2.CLICKABLE 和 LONG_CLICKABLE 只要有一个为 true 就消费这个事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    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) {
                            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) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    // 3.在 ACTION_UP 方法发生时会触发 performClick() 方法
                                    performClick();
                                }
                            }
                        }
                        ...
                    break;
            }
            ...
            return true;
        }
        return false;
    }

上述代码有三个关键点分别在注释处标出。

  • 可以看出即便 View 是 disabled 状态,依然不会影响事件的消费,只是它看起来不可用
  • 只要 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,就一定会消费这个事件,就是 onTouchEvent 返回 true,这点也印证了我们前面说的 View 的 onTouchEvent 方法默认都会消费掉事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false),View 的 longClickable 默认为 false,clickable 需要区分情况,如 Button 的 clickable 默认为 true,而 TextView 的 clickable 默认为 false

ACTION_UP 方法中有 performClick();接下来看一下它:

  // view.java
  public boolean performClick() {
        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);
        return result;
    }

很明显,如果 View 设置了 OnClickListener,那么会回调 onClick 方法。

最后再强调一点,我们刚说过 View 的 longClickable 默认为 false,clickable 需要区分情况,如 Button 的 clickable 默认为 true,而 TextView 的 clickable 默认为 false。

这是默认情况,我们可以单独给 View 设置 clickable 属性,但有时候会发现 View 的 setClickable 方法失效了。假如我们想让 View 默认不可点击,将 View 的 clickable 设置成 false,在合适的时候需要可点击所以我们又给 View 设置了 OnClickListener,那么你会发现 View 默认依然可以点击,也就是说 setClickable 失效了。

// view.java
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

View 的 setOnClickListener 会默认将 View 的 clickable 设置成 true。
View 的 setOnLongClickListener 同样会将 View 的 longClickable 设置成 true。

3.7 onTouch 和 onClick 执行的位置和关系

onTouch方法中返回true的时候,onClick方法不执行,当返回false的时候,onClick方法执行

 onTouch方法中返回true时, 短路与,当result 为true ,onTouchEvent不执行而onClick是在onTouchEvent up事件时候执行的,所以OnClick事件不执行

public boolean dispatchTouchEvent(MotionEvent event) {
        	....
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) { // 短路与,当result 为true ,onTouchEvent不执行
                result = true;
            }
        
			....
        return result;
}
public boolean onTouchEvent(MotionEvent event) {
        ...
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                 				 ...
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }

                       			 ...
      
private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }


public boolean performClick() {
        ...
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this); // 表示该事件 这个View消费了
            result = true;
        } else {
            result = false;
        }
		...
    }

3.8 requestDisallowInterceptTouchEvent方法

@Override
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);
    }
}

可以看到它改变了一个开关FLAG_DISALLOW_INTERCEPT,同时调用其parent的函数。那么这个开关有什么用?在ViewGroup的dispatchTouchEvent函数开头有这样一段代码:

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); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

 一开始就使用了FLAG_DISALLOW_INTERCEPT开关,即disallowInterceptdisallowIntercept为true,则不拦截;否者判断onInterceptTouchEvent所以简单来说requestDisallowInterceptTouchEvent设置为true可以跳过onInterceptTouchEvent,不拦截事件。

而且因为requestDisallowInterceptTouchEvent又调用了parent的函数,所以所有层次的父view都不再拦截。

所以requestDisallowInterceptTouchEvent的功能是让这个view及上面的所有父view都放开拦截,即使onInterceptTouchEvent为true。

一般在自定义view里使用

getParent().requestDisallowInterceptTouchEvent(true);

这样view的所有层次的父view都不会拦截事件了。不加getParent直接requestDisallowInterceptTouchEvent(true) 也可以让上面的所有父view都放开拦截,但是会自己这个view对其子view的拦截关系。所以使用getParent().requestDisallowInterceptTouchEvent(true)。

四 实例解析


**父view是ViewPager,子view是ListView**
 1. viewPager onInterceptTouchEvent 为true --》 上下滑动不可以,左右可以
 2. viewPager  onInterceptTouchEvent 为false --》 上下滑动可以,左右不可以
 3. viewPager  onInterceptTouchEvent 为false, ListView重写 dispatchTouchEvent 返回false --》 上下不可以,左右可以
 4. 如何实现上下可以,左右也可以?

4.1 viewPager onInterceptTouchEvent 为true

// 父容器
public class BadViewPager extends ViewPager {
    
    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        Log.w("chric", "BadViewPager onInterceptTouchEvent:" +event.getAction());
        return true;
    }
    

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("chric", "BadViewPager dispatchTouchEvent:" +ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric", "BadViewPager onTouchEvent:" +ev.getAction());
        return super.onTouchEvent(ev);
    }
}
public class MyListView extends ListView {

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("chric","MyListView dispatchTouchEvent:" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric","MyListView onTouchEvent:" + ev.getAction());
        return super.onTouchEvent(ev);
    }
}

分析:

Action_Down事件:
intercepted = onInterceptTouchEvent(ev)为true --》 if (!canceled && !intercepted)进不去 --》mFirstTouchTarget 为null --》
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS) —》ViewPager自己处理down事件
Move事件:
mFirstTouchTarget 为null,第一部分代码走else --》 intercepted = true; —》handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS) —》viewPager自己处理move事件

日志:

上下或者左右滑动时,事件都被viewpager消费,所以上下滑动不可以,viewpager左右可以滑动

// down
2024-04-27 19:45:11.607 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 19:45:11.608 7641-7641/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 19:45:11.608 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:0
// move
2024-04-27 19:45:11.627 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:45:11.627 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 19:45:11.644 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:45:11.644 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 19:45:11.660 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:45:11.660 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 19:45:11.677 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:45:11.677 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 19:45:11.689 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:45:11.689 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
// up
2024-04-27 19:45:11.690 7641-7641/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 19:45:11.690 7641-7641/com.chric.dispatch W/chric: BadViewPager onTouchEvent:1

 4.2 viewPager  onInterceptTouchEvent 为false

// 父容器
public class BadViewPager extends ViewPager {

    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        Log.w("chric", "BadViewPager onInterceptTouchEvent:" +event.getAction());
        return false;
    }
    

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("chric", "BadViewPager dispatchTouchEvent:" +ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric", "BadViewPager onTouchEvent:" +ev.getAction());
        return super.onTouchEvent(ev);
    }
}
public class MyListView extends ListView {

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("chric","MyListView dispatchTouchEvent:" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric","MyListView onTouchEvent:" + ev.getAction());
        return super.onTouchEvent(ev);
    }
}

分析:

Action_Down:
intercepted = onInterceptTouchEvent(ev)为false --》dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
询问listView是否处理down事件,返回true --》alreadyDispatchedToNewTouchTarget 为true --》第三部分代码走else --》

     if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    }

Action_Move事件:
mFirstTouchTarget != null

        if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // disallowIntercept 决定onInterceptTouchEvent 是否执行
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } 

–》alreadyDispatchedToNewTouchTarget为false,分发时才赋值 --》dispatchTransformedTouchEvent(ev, cancelChild,
ListView, target.pointerIdBits) -->listView处理move事件–》handled = true;

     final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            handled = listView.dispatchTouchEvent(transformedEvent);
        }
        

日志:

上下滑动:listview默认处理上下滑动事件,事件被listview消费了

// down
2024-04-27 19:59:57.145 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 19:59:57.146 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 19:59:57.146 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 19:59:57.146 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:0
// move
2024-04-27 19:59:57.160 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.160 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 19:59:57.160 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.160 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.176 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.176 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 19:59:57.176 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.176 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.193 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.193 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 19:59:57.193 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.193 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.210 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.210 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 19:59:57.211 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.211 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.227 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.227 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.228 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.243 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.243 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.244 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.260 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.260 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.260 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 19:59:57.268 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 19:59:57.269 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 19:59:57.269 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
// up
2024-04-27 19:59:57.270 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 19:59:57.270 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:1
2024-04-27 19:59:57.270 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:1

左右滑动:listview默认处理左右滑动事件,事件被listview消费了,所以viewpager不能左右滑动

// down
2024-04-27 20:01:44.752 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 20:01:44.753 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 20:01:44.753 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 20:01:44.753 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:0
// move
2024-04-27 20:01:44.827 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:01:44.827 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:01:44.827 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:01:44.828 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:01:44.843 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:01:44.843 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:01:44.843 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:01:44.843 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:01:44.861 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:01:44.861 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:01:44.861 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:01:44.861 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2

2024-04-27 20:01:44.876 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:01:44.876 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:01:44.876 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:01:44.876 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:01:44.890 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:01:44.890 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:01:44.890 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:01:44.890 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:2
// up
2024-04-27 20:01:44.891 8261-8261/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 20:01:44.891 8261-8261/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:1
2024-04-27 20:01:44.891 8261-8261/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:1
2024-04-27 20:01:44.891 8261-8261/com.chric.dispatch W/chric: MyListView onTouchEvent:1

4.3 viewPager  onInterceptTouchEvent 为false, ListView重写 dispatchTouchEvent 返回false

分析:

Action_Down:
intercepted = onInterceptTouchEvent(ev)为false --》dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
询问listView是否处理down事件 --》handled = listView.dispatchTouchEvent(event)返回false,ViewPager只有一个孩子退出循环 --》 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),ViewPager自己处理down事件

Action_Move事件:
mFirstTouchTarget == null,viewPager自己处理

日志:

上下滑动:

// down
2024-04-27 20:12:18.518 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 20:12:18.518 8629-8629/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 20:12:18.518 8629-8629/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 20:12:18.518 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:0
// move
2024-04-27 20:12:18.526 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.526 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.543 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.543 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.560 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.561 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.577 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.577 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.593 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.594 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.609 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.609 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.626 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.626 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.644 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.644 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:12:18.647 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:12:18.647 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
// up
2024-04-27 20:12:18.648 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 20:12:18.648 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:1

左右滑动:

// down
2024-04-27 20:14:08.672 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 20:14:08.672 8629-8629/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 20:14:08.672 8629-8629/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 20:14:08.672 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:0
// move
2024-04-27 20:14:08.726 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:14:08.726 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:14:08.743 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:14:08.744 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:14:08.760 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:14:08.760 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:14:08.780 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:14:08.780 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:14:08.795 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:14:08.795 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:14:08.806 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:14:08.806 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
// up
2024-04-27 20:14:08.806 8629-8629/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 20:14:08.807 8629-8629/com.chric.dispatch W/chric: BadViewPager onTouchEvent:1

五 事件冲突

5.1 滑动冲突的常见场景

当我们内外两层 View 都可以滑动时候,就会产生滑动冲突。

常见的滑动冲突场景:

  • 外层与内层滑动方向不一致,外层 ViewGroup 是可以横向滑动的,内层 View 是可以竖向滑动的(类似 ViewPager,每个页面里面是 ListView)

  • 外层与内层滑动方向一致,外层 ViewGroup 是可以竖向滑动的,内层 View 同样也是竖向滑动的(类似 ScrollView 包裹 ListView)

  • 当然还有上面两种组合起来,三层或者多层嵌套产生的冲突,然而不管是多么复杂,解决的思路都是一模一样。所以遇到多层嵌套的小伙伴也不用惊慌,一层一层处理即可。

有小伙伴肯定有疑问,ViewPager 带 ListView 并没有出现滑动冲突啊。那是因为 ViewPager 已经为我们处理了滑动冲突!如果我们自己定义一个水平滑动的 ViewGroup 内部再使用 ListView,那么是一定需要处理滑动冲突的。

针对上面的第一种场景,由于外部与内部的滑动方向不一致,那么我们可以根据当前滑动方向,水平还是垂直来判断这个事件到底该交给谁来处理。至于如何获得滑动方向,我们可以得到滑动过程中的两个点的坐标。一般情况下根据水平和竖直方向滑动的距离差就可以判断方向,当然也可以根据滑动路径形成的夹角(或者说是斜率如下图)、水平和竖直方向滑动速度差来判

针对第二种场景,由于外部与内部的滑动方向一致,那么不能根据滑动角度、距离差或者速度差来判断。这种情况下必需通过业务逻辑来进行判断。比较常见 ScrollView 嵌套了 ListView。虽然需求不同,业务逻辑自然也不同,但是解决滑动冲突的方式都是一样的。

微博的这个是同方向,竖向滑动冲突的场景,可以看到发现布局整体是可以滚动的,而且下方的微博列表也是可以滚动的。根据业务逻辑,当热门,榜单…这一行标签栏滑动到顶部的时候微博列表才可以滚动。否则就是布局的整体滚动。这个场景是不是在很多 app 里面都能够见到呢?

天猫的这个是同方向,横向滑动冲突的场景,内外两层都是可以横向滚动的。它的处理逻辑也很明显,根据用户滑动的位置来判断到底是那个 View 需要响应滑动。

上面两种滑动冲突的场景区别只是在于拦截的逻辑处理上。第一种是根据水平还是竖直滑动来判断谁来处理滑动,第二种是根据业务逻辑来判断谁来处理滑动,但是处理的套路都是一样的


5.2 滑动冲突解决套路

5.2.1 外部拦截法:

即父 View 根据需要对事件进行拦截。逻辑处理放在父 View 的 onInterceptTouchEvent 方法中。我们只需要重写父 View 的onInterceptTouchEvent 方法,并根据逻辑需要做相应的拦截即可。

    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (满足父容器的拦截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

 上面伪代码表示外部拦截法的处理思路,需要注意下面几点:

  • 根据业务逻辑需要,在 ACTION_MOVE 方法中进行判断,如果需要父 View 处理则返回 true,否则返回 false,事件分发给子 View 去处理。
  • ACTION_DOWN 一定返回 false,不要拦截它,否则根据 View 事件分发机制,后续 ACTION_MOVE 与 ACTION_UP 事件都将默认交给父 View 去处理
  • 原则上 ACTION_UP 也需要返回 false,如果返回 true,而当滑动事件是交给子 View 处理的时候,那么子 View 将接收不到 ACTION_UP 事件,子 View 的 onClick 事件也无法触发。而父 View 不一样,如果父 View 在 ACTION_MOVE 中开始拦截事件,那么后续 ACTION_UP 也将默认交给父 View 处理

5.2.2 内部拦截法:

即父 View 不拦截任何事件,所有事件都传递给子 View,子 View 根据需要,决定是自己消费事件还是给父 View 处理。这需要子 View 使用 requestDisallowInterceptTouchEvent 方法才能正常工作。下面是子 View 的 dispatchTouchEvent 方法的伪代码:

    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

父 View 需要重写 onInterceptTouchEvent 方法:

    public boolean onInterceptTouchEvent(MotionEvent event) {

        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

使用内部拦截法需要注意:

  • 内部拦截法要求父 View 不能拦截 ACTION_DOWN 事件,由于 ACTION_DOWN 不受 FLAG_DISALLOW_INTERCEPT 标志位控制,一旦父容器拦截 ACTION_DOWN 那么所有的事件都不会传递给子 View
  • 滑动策略的逻辑放在子 View 的 dispatchTouchEvent 方法的 ACTION_MOVE 中,如果父容器需要获取点击事件则调用 parent.requestDisallowInterceptTouchEvent(false) 方法,让父容器去拦截事件

5.3 内部拦截法实现实现上下可以,左右也可以

// 父容器
public class BadViewPager extends ViewPager {

    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    
    // 默认viewpager是不拦截的
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        Log.w("huangben", "BadViewPager onInterceptTouchEvent:" +event.getAction());
        return super.onInterceptTouchEvent(event);
    }

    // 重新ViewPager这样也行
//    @Override
//    public boolean onInterceptTouchEvent(MotionEvent event) {
//        Log.w("huangben", "BadViewPager onInterceptTouchEvent:" +event.getAction());
//        if (event.getAction() == MotionEvent.ACTION_DOWN) {
//            super.onInterceptTouchEvent(event);
//            return false;
//        }
//        return true;
//    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("huangben", "BadViewPager dispatchTouchEvent:" +ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("huangben", "BadViewPager onTouchEvent:" +ev.getAction());
        return super.onTouchEvent(ev);
    }
}
// 子View -- 容器 -- ViewGroup.dispatchTouchEvent --> super.dispatchTouchEvent
public class MyListView extends ListView {

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private int mLastX, mLastY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.w("chric","MyListView dispatchTouchEvent:" + event.getAction());
        int x = (int) event.getX();
        int y = (int) event.getY();


        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.w("chric","MyListView dispatchTouchEvent MyListView请求父亲BadViewPager不拦截:" + event.getAction());
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                // 这个条件由业务逻辑决定,看什么时候 子View将事件让出去
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    Log.w("chric","MyListView dispatchTouchEvent MyListView请求父亲BadViewPager拦截:" + event.getAction());
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;

            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric","MyListView onTouchEvent:" + ev.getAction());
        return super.onTouchEvent(ev);
    }
}

分析:

down事件 :
–》 分发给了ListView处理 --》listView处理Down事件 --》getParent().requestDisallowInterceptTouchEvent(true)请求viewPage不拦截

第一个MOVE事件

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } 

竖直移动
mGroupFlags |= FLAG_DISALLOW_INTERCEPT再& FLAG_DISALLOW_INTERCEPT != 0 为ture, disallowIntercept 为true,
intercepted = false --》dispatchTransformedTouchEvent询问listView是否处理
水平移动:
dispatchTransformedTouchEvent询问listView是否处理 --》listView处理第一个Move事件 --》getParent().requestDisallowInterceptTouchEvent(false)

第二个MOVE事件:

–》disallowIntercept 为false --》intercepted = onInterceptTouchEvent(ev) viewPager返回true—》dispatchTransformedTouchEvent cancelChild为true --》

                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                         // intercepted为true时cancelChild为true       
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        // 为true 取消child处理事件
                        if (cancelChild) {
                            if (predecessor == null) {
                                // mFirstTouchTarget置为null
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                 

–>listView.dispatchTouchEvent(MotionEvent.ACTION_CANCEL)

      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;
        }

—> mFirstTouchTarget = next mFirstTouchTarget 置为null

                        if (cancelChild) {
                            if (predecessor == null) {
                                // mFirstTouchTarget置为null
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }

后续MOVE事件

     if (mFirstTouchTarget == null) {
                viewPager处理后续move事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 

日志:

上下滑动:

// down
2024-04-27 20:44:28.151 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 20:44:28.151 9153-9153/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 20:44:28.151 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 20:44:28.151 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent MyListView请求父亲BadViewPager不拦截:0
2024-04-27 20:44:28.151 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:0
// move  disallowIntercept为true 决定onInterceptTouchEvent不执行
2024-04-27 20:44:28.160 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.160 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.160 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:44:28.177 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.177 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.177 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:44:28.193 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.193 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.193 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:44:28.210 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.210 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.210 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:44:28.226 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.226 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.227 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:44:28.243 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.243 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.244 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:44:28.256 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:44:28.256 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:44:28.256 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
// up
2024-04-27 20:44:28.257 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 20:44:28.258 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:1
2024-04-27 20:44:28.258 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:1

左右滑动:

// down
2024-04-27 20:45:35.588 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 20:45:35.588 9153-9153/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 20:45:35.589 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 20:45:35.589 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent MyListView请求父亲BadViewPager不拦截:0
2024-04-27 20:45:35.589 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:0
// 第一个move  disallowIntercept为true 决定onInterceptTouchEvent不执行
2024-04-27 20:45:35.626 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:45:35.626 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:45:35.626 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent MyListView请求父亲BadViewPager拦截:2
2024-04-27 20:45:35.626 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:2
// 第二个move  disallowIntercept为false 决定onInterceptTouchEvent执行
2024-04-27 20:45:35.643 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:45:35.643 9153-9153/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
// listview cancel事件
2024-04-27 20:45:35.644 9153-9153/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:3
2024-04-27 20:45:35.644 9153-9153/com.chric.dispatch W/chric: MyListView onTouchEvent:3
// 后续move
2024-04-27 20:45:35.662 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:45:35.663 9153-9153/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:45:35.679 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:45:35.679 9153-9153/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:45:35.697 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:45:35.698 9153-9153/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 20:45:35.708 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:45:35.708 9153-9153/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
// up
2024-04-27 20:45:35.711 9153-9153/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 20:45:35.711 9153-9153/com.chric.dispatch W/chric: BadViewPager onTouchEvent:1

5.4 外部拦截法实现实现上下可以,左右也可以

// 父容器
public class BadViewPager extends ViewPager {
    private int mLastX, mLastY;

    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    // 外部拦截法:一般只需要在父容器处理,根据业务需求,返回true或者false
    public boolean onInterceptTouchEvent(MotionEvent event) {
        Log.w("chric", "BadViewPager onInterceptTouchEvent:" +event.getAction());
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = (int) event.getX();
                mLastY = (int) event.getY();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    Log.w("chric", "BadViewPager onInterceptTouchEvent 拦截:" +event.getAction());
                    return true;
                }
                Log.w("chric", "BadViewPager onInterceptTouchEvent 不拦截:" +event.getAction());
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.w("chric", "BadViewPager dispatchTouchEvent:" +ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric", "BadViewPager onTouchEvent:" +ev.getAction());
        return super.onTouchEvent(ev);
    }
}
// 子View -- 容器 -- ViewGroup.dispatchTouchEvent --> super.dispatchTouchEvent
public class MyListView extends ListView {

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.w("chric","MyListView dispatchTouchEvent:" + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.w("chric","MyListView onTouchEvent:" + ev.getAction());
        return super.onTouchEvent(ev);
    }
}

日志:

上下滑动:

// down
2024-04-27 20:57:59.102 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 20:57:59.102 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 20:57:59.102 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 20:57:59.102 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:0
// move
2024-04-27 20:57:59.111 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.111 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:57:59.111 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent 不拦截:2
2024-04-27 20:57:59.111 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.111 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.126 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.126 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:57:59.127 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent 不拦截:2
2024-04-27 20:57:59.127 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.127 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.143 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.144 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 20:57:59.144 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent 不拦截:2
2024-04-27 20:57:59.144 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.144 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.159 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.159 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.160 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.176 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.176 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.176 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.194 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.194 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.194 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.209 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.210 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.210 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
2024-04-27 20:57:59.226 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 20:57:59.226 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:2
2024-04-27 20:57:59.226 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:2
// up
2024-04-27 20:57:59.226 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 20:57:59.227 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:1
2024-04-27 20:57:59.227 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:1

左右滑动:

// down
2024-04-27 21:01:15.059 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:0
2024-04-27 21:01:15.059 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:0
2024-04-27 21:01:15.059 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:0
2024-04-27 21:01:15.060 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:0
2024-04-27 21:01:15.076 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
// 第一个move listview处理变成cancel
2024-04-27 21:01:15.077 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent:2
2024-04-27 21:01:15.077 9327-9327/com.chric.dispatch W/chric: BadViewPager onInterceptTouchEvent 拦截:2
// listview cancel事件
2024-04-27 21:01:15.077 9327-9327/com.chric.dispatch W/chric: MyListView dispatchTouchEvent:3
2024-04-27 21:01:15.077 9327-9327/com.chric.dispatch W/chric: MyListView onTouchEvent:3
// 后续move
2024-04-27 21:01:15.095 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 21:01:15.095 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 21:01:15.127 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 21:01:15.127 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 21:01:15.143 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 21:01:15.143 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 21:01:15.160 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 21:01:15.160 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 21:01:15.177 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 21:01:15.177 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
2024-04-27 21:01:15.186 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:2
2024-04-27 21:01:15.186 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:2
// up
2024-04-27 21:01:15.186 9327-9327/com.chric.dispatch W/chric: BadViewPager dispatchTouchEvent:1
2024-04-27 21:01:15.186 9327-9327/com.chric.dispatch W/chric: BadViewPager onTouchEvent:1

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值