还不知道事件分发的朋友可以先去看看我的另一篇事件分发后再来看事件冲突——事件分发

【1】什么是事件冲突?

简单来说就是,一个动作(down,move等)只能由一个View或者ViewGroup处理,而有时我们希望ViewA处理这个事件,但是却是ViewB处理的这个事件,这时我们就说产生了时间冲突。

【2】解决事件冲突的常用方案

注意:解决事件冲突只能在Move动作的时候

①外部处理

外部处理就是在希望处理的子View的父容器中处理

②内部处理

直接在希望处理的容器中处理

【3】先来过一遍Move的处理流程(源码角度) 一定记得Move事件是多次调用的

①不拦截情况

   public boolean dispatchTouchEvent(MotionEvent ev) {
            .......
            .......
            .......
            .......
               // 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);
            } else {
                // 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 (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            .......
            .......
            .......
            .......

由于是Move事件,mFirstTouchTarget在分发(down事件)的时候已经赋值了(不知道可以参考我的事件分发的文章)所以不为null,进入else语句
alreadyDispatchedToNewTouchTarget 在上面是置空的,所以这里进入while的else语句

 final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                       

以上代码是重点;此时intercepted为false,target.child这个参数是事件分发的时候储存的,进入dispatchTransformedTouchEvent方法

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

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        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;
        }
        ......
        .....
        .....

执行child.dispatchTouchEvent(event);到此一次Move执行完毕

②拦截情况(父容器设置拦截)

同样先进入父容器的dispatchTouchEvent

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

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

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            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;
            }

此时intercepted为true,同样也来到这里:

// 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);
            } else {
                // 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 (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }

mFirstTouchTarget不为空,进入else语句块,同样也进入while中的else中:

final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }

注意了,这里开始不同了,由于intercepted为true,所以cancelChild 为true,进入dispatchTransformedTouchEvent中:

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

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        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;
        }

此时我们进入if语句中,调用孩子的handled = child.dispatchTouchEvent(event);但是我们注意,这里的event已经被改为了MotionEvent.ACTION_CANCEL调用了孩子的cancel,完成后,我们回退到上一张图进入到if (cancelChild)

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

这里将mFirstTouchTarget = next;而next在事件分发的时候(单点)是null,也就是mFirstTouchTarget = null。到此拦截情况的一次流程已经走完了。你可能会产生疑问了:这里已经Move事件都完了,但是还没有开始处理的,因为孩子处理的是cancel根本就不是Move,因为被父容器拦截了。那父容器为什么没有处理这个事件呢? 此时我们的父容器根本就没有处理这个事件,第一次的Move事件只是为了两件事情:1.孩子执行cancel2.将mFirstTouchTarget = null

那么父容器什么时候处理这个事件呢?上面说了一定记得Move是多次触发的,并不止执行一次

拦截情况的第二次Move事件:

第二次intercepted还是为true,来到老位置:

 if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 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 (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

我们以前都是走的else,现在由于mFirstTouchTarget = null,直接进入if语句,执行一下内容:

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

到此是不是已经明白了,父容器已经拦截,并且自己处理了这个事件。

【4】一个例子让你深刻明白事件冲突及上文内容

例:我们假设:ViewPager中放了一个RecyclerView:
在这里插入图片描述
此时可能会碰到这样一个问题,上下滑动的时候我们希望滑动RecyclerView,单击的时候也想是点击RecyclerView,而左右滑动的时候我想,滑动ViewPager,那我们要想达到这个效果就需要来解决这个冲突。

解决方案

①内部处理思路

父容器在oninterceptTouchEvent返回true
子容器根据不同的点击事件来设置 getParent().requestDisallowInterceptTouchEvent表示是否让父容器拦截,参数填true,表示父容器不能拦截。当down事件时我们设置父容器不能拦截,事件分发到这个子View后,后面子View是否要交给父容器处理由这个子View决定。再当横向滑动的时候,我们设置 getParent().requestDisallowInterceptTouchEvent为false,这个时候父容器拦截,这个子View就不能处理这个事件了。

问题:发现这样以后不能达到预期的效果。

原因:当点击事件为Down时,getParent().requestDisallowInterceptTouchEvent(true)是无效的,为什么?我们来看源码:

 // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            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;
            }

看到

  if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

如果为Down事件的时候,下面有两个清空和重置的操作,而我们刚才设置的true会被重置

if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }

导致我们不能进入else,而只能进入if,但是if中会将intercept设置为True,所以Down事件被拦截了,没有分发到RecyclerView上。

解决:

在这里插入图片描述
在父容器中,如果为Down事件我就不拦截,直接返回false。

②外部处理思路

直接在父容器中也就是ViewPager中的oninterceptTouchEvent,根据不同的情况选择是否进行拦截
在这里插入图片描述
如上,如果是横向滑动的话就进行拦截,交给我们的ViewPager进行处理。

记住这几句话:

  • 父容器可以抢子View的事件进行处理
  • 子View不能抢父容器的事件进行处理
  • 子View一旦拿到事件,事件再交给谁处理就是子View说了算了。
  • 同时要明白一个误区,不是说必须拿到Down事件才能处理其他的事件如Move等。如果对子View,叶子来说没什么问题,但是就像上面的例子一样,父容器没有拿到Down事件,但是却可以处理Move事件。
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐