Android 事件分发和 View 的滑动冲突

Android 事件分发和滑动冲突都是开发中经常遇到的难点问题,遇到问题时可能会通过 Google 或者 StackOverflow 按照别人的经验解决了问题,但每次遇到这种问题都去 Google 也是非常不合适的事情。本篇文章将从 Android 事件分发的源码入手,首先分析源码,当我们了解了源码,不但能从源码中总结到常见问题的解决方式,并且遇到更加深入问题时也能冷静的从源码入手来解决问题,做到知其然更知其所以然。滑动冲突问题究其根本其实就是事件分发问题,了解了事件分发,我们也就能从一定的高度来解决滑动冲突问题,并总结出解决滑动冲突问题的模式。

一、Android UI 界面架构

要了解 Android 事件分发,我们要先来了解一下 Android 的 UI 界面架构,因为事件的分发流程是以 Android 界面架构为基础的,以一张图来介绍
Android UI 界面框架

如图所示,每一个 Activity 都包含一个 Window,Android 中 Window 的实现类是 PhoneWindow;PhoneWindow 中包含一个 DecorView,也就是一个界面布局的根 View,一般是一个 FrameLayout;DecorView 中有一个 ContentView 实际上是一个 ViewGroup,看名字很熟悉,其实这个 ViewGroup 就是 Activity 中我们要显示布局的 View 对象的父容器,一般是一个 FrameLayout,在 Activity 的 onCreate() 中通过 setContentView 方法将要显示的布局的 View 对象放入该 ContentView;ContentView 中的 ViewGroup 就是我们界面要显示的布局 ViewGroup;View 则是界面中每一个需要显示的 View 控件,这就是 Android UI 界面框架的简单模型。

二、Android 事件分发

接着说事件分发,一个触摸事件的产生是由屏幕、Native 层、Framework 层产生的,产生之后会通过 Framework 层传递到 Activity 中,刚才提到了事件分发过程是以 Android 界面架构为基础的,怎么理解呢,就是说事件的分发流程是以 Activity 开始,经过 PhoneWindow、DecorView、ViewGroup、View,整个过程正好与界面架构的层级匹配。

由于 DecorView 也是 GroupView,为了简单,在分析整个事件分发流程时我们可以把 DecorView、ContentView、GroupView 合一,直接分析 Activity、PhoneWindow、ViewGroup、View 四个层级

注意:每个事件都会经历以下所说的所有流程,并且一个事件执行结束后才开始执行下一事件,例如一次点击:ACTION_DOWN ACTION_MOVE ACTION_UP,这里就会产生三个事件,这三个事件属于同一个事件系列,三个事件都会经历以下所说的所有流程

事件在 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法开始,来看一下该方法

// Acitivty
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); // 是个空方法
    }
    if (getWindow().superDispatchTouchEvent(ev)) { // 将事件传递到 PhoneWindow
        return true;
    }
    return onTouchEvent(ev); // 如果 Window 没有处理则 Activity 自己处理
}

// Activity 的处理逻辑是默认关闭事件,如果需要 Activity 处理,则需要开发者重写该方法
public boolean onTouchEvent(MotionEvent event) {

    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

// PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event); // mDecor 即为 DecorView
}

先稍微梳理一下,Activity 的 dispatchTouchEvent 方法中会调用 PhoneWindow 中的 superDispatchTouchEvent 方法,如果该方法返回 true 那么事件处理结束,如果该方法返回 false ,那么调用 Activity 的 onTouchEvent 方法 Activity 自己来处理事件。

PhoneWindow 中的 superDispatchTouchEvent 方法中直接调用 mDecor.superDispatchTouchEvent(event) ,这里的 mDecor 就是界面框架中的 DecorView,DecorView 的 superDispatchTouchEvent 方法直接调用 ViewGroup 的 dispatchTouchEvent 方法,这里就讲事件传递到了 ViewGroup 中,下面的内容就是本篇文章的重中之重,现在开始吧。

1. ViewGroup 中的事件分发和处理

这里先来稍微介绍一下事件如何在 ViewGroup 中分发,然后在根据源码来理解即可,点击事件到达顶级 View(一般是 ViewGroup) 后,会调用 ViewGroup 的 dispatchTouchEvent 来进行事件分发,逻辑如下:

  • 简单来说,ViewGroup 的 dispatchTouchEvent 方法首先根据 onInterceptTouchEvent 方法以及一些其他条件来判断是否拦截该事件,如果 ViewGroup 拦截该事件,那么调用 ViewGroup 的 onTouchEvent 来处理事件,并且不会将事件传递到子 View,如果不拦截则将事件传递到子 View 来处理。不管是 ViewGroup 自己处理该事件还是传递到子 View 处理,dispatchTouchEvent 都会接收事件的处理结果,并将事件的处理结果返回到上一层

  • ViewGroup.dispatchTouchEvent() 完成事件分发,并接收事件处理结果,最后将事件的处理结果返回到上一层

  • ViewGroup.onInterceptTouchEvent() 方法判断当前 ViewGroup 是否拦截此事件

  • View.onTouchEvent() 事件处理,并将事件的处理结果返回

好啦,下面就从源码开始来一点点分析,由于 ViewGroup 的 dispatchTouchEvent 方法比较长,所以下面会一段一段来分析



// ViewGroup.dispatchTouchEvent
{
    // Handle an initial down. ACTION_DOWN 事件时将 FLAG_DISALLOW_INTERCEPT 标记重置为关闭
    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();
    }
    
    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;
    }
}

// ViewGroup
public boolean onInterceptHoverEvent(MotionEvent event) {
    return false;
}

上面这段代码的作用就是确定 ViewGroup 是否需要拦截事件

先来解释一下里面的对象,mFirstTouchTarget 默认为 null,在有子 View 处理了 ACTION_DOWN 事件时会赋值,一个事件系列中第一个事件到来时 mFirstTouchTarget 肯定为 null

FLAG_DISALLOW_INTERCEPT,一个标记,通过子 View 中调用 getParent().requestDisallowInterceptTouchEvent() 方法修改此标记,如果开启此标记,表示 ViewGroup 不会拦截事件,该标记在 ACTION_DOWN 事件到来时会重置此标记为关闭状态。

由代码来分析,一个事件系列中,第一次来的总是 ACTION_DOWN,此时 mFirstTouchTarget 也为 null,且 FLAG_DISALLOW_INTERCEPT 为关闭,此时会调用 onInterceptTouchEvent(ev) 方法来判断是否拦截, ViewGroup 默认不拦截任何事件。

如果事件不是 ACTION_DOWN ,且 mFirstTouchTarget 为 null,说明在 ACTION_DOWN 事件时没有子 View 处理了事件或者是 ViewGroup 拦截了 ACTION_DOWN 事件,那么同一系列事件都不会再传递到子 View,那么 ViewGroup 直接拦截事件;此时会调用 dispatchTransformedTouchEvent 方法,该方法中会调用 super.dispatchTouchEvent(event) ,该方法为父类的 dispatchTouchEvent ,其中会调用处理事件的方法,下面会分析源码。最后将处理结果返回上一层。

如果 mFirstTouchTarget 则说明有处理了 ACTION_DOWN 事件的 View ,则会继续通过 onInterceptTouchEvent 来判断是否需要拦截事件,在判断是还会受 FLAG_DISALLOW_INTERCEPT 的影响,如果 FLAG_DISALLOW_INTERCEPT 开启,那么只要 mFirstTouchTarget 有值,ViewGroup 都不会拦截事件,如果 mFirstTouchTarget 关闭则根据 onInterceptTouchEvent 方法的返回值来决定是否拦截。

上面确定了是否需要拦截事件,接着看 ViewGroup 的 dispatchTouchEvent 方法的源码中在拦截和非拦截情况下事件是怎么处理的

// ViewGroup.dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
    
    // 判断是否拦截
    ... 
    
    // If intercepted, start normal event dispatch. Also if there is already
    // a view that is handling the gesture, do normal event dispatch.
    if (intercepted || mFirstTouchTarget != null) { 
        ev.setTargetAccessibilityFocus(false);
    }

    if (!canceled && !intercepted) { // 如果不拦截
        if (actionMasked == MotionEvent.ACTION_DOWN...) { // ACTION_DOWN 时该判断为真,会执行其中的方法
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            ...
            for (int i = childrenCount - 1; i >= 0; i--) { // 遍历所有子 View 
                final int childIndex = customOrder
                        ? getChildDrawingOrder(childrenCount, i) : i;
                final View child = (preorderedList == null)
                        ? children[childIndex] : preorderedList.get(childIndex);
    
                // If there is a view that has accessibility focus we want it
                // to get the event first and if not handled we will perform a
                // normal dispatch. We may do a double iteration but this is
                // safer given the timeframe.
                if (childWithAccessibilityFocus != null) {
                    if (childWithAccessibilityFocus != child) {
                        continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount - 1;
                }
    
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
    
                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);
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 调用子 View 来处理事件
                    // 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();
                    newTouchTarget = addTouchTarget(child, idBitsToAssign); // 子 View 处理了事件,则将 mFirstTouchTarget 赋值并终止遍历子 View
                    alreadyDispatchedToNewTouchTarget = true; // 将事件处理结果置为 true 表示已经有子 View 处理了事件
                    break; // 跳出循环
                }
    
                // The accessibility focus didn't handle the event, so clear
                // the flag and do a normal dispatch to all children.
                ev.setTargetAccessibilityFocus(false);
            }
            ...
        }
        
        if (mFirstTouchTarget == null) { // 如果 mFirstTouchTarget 为 null,说明是 ACTION_DOWN 事件且没有子 View 处理事件,直接调用 ViewGroup 的 dispatchTransformedTouchEvent ,并且其中调用父类的 dispatchTouchEvent 方法处理事件并将该方法返回值赋值到处理结果
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        }else {
            
            // 执行到这里说明 ViewGroup 拦截了事件或者是 mFirstTouchTarget 不为 null
            
            // 如 ViewGroup 拦截事件就调用父类的 dispatchTouchEvent 方法处理事件并将该方法返回值赋值到处理结果 
            
            // 如果 ViewGroup 不拦截事件,mFirstTouchTarget 有值,通过 dispatchTransformedTouchEvent 方法调用调用 target.child 处理事件,并将该方法返回值赋值到处理结果
            
            // 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) { // 如果已经有子 View 处理了事件,则将 true 赋值处理结果
                    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;
            }
        }
    }
    
    // ViewGroup 有子 View 处理了 ACTION_DOWN 事件时为 mFirstTouchTarget 赋值
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target; // mFirstTouchTarget 赋值
        return target;
    }
    
    // ViewGroup
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        
        ...
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);  // 子 View 为空,调用父类 dispatchTouchEvent 方法处理事件
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            // 子 View 不为空,调用子 View 的 dispatchTouchEvent 方法
            handled = child.dispatchTouchEvent(transformedEvent);
        }

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

这段代码也很简单,主要就是根据上面 ViewGroup 是否拦截此事件以及此事件的事件类型来处理事件,下面就按照源码的流程来分析。

  1. 如果 ViewGroup 不拦截事件且事件为 ACTION_DOWN 时遍历子 View,寻找出符合条件的子 View 来处理事件,寻找的条件也很简单,主要就是触摸事件是否落到该 View 所在区域与该 View 是否在播动画 ,如果找到符合条件的子 View,就会调用 dispatchTransformedTouchEvent ,在该方法中调用子 View 的 dispatchTouchEvent 方法,将事件传递到子 View 中,如果子 Viwe 处理了该事件则为 mFirstTouchTarget 赋将子 View 是否处理了事件的标记 alreadyDispatchedToNewTouchTarget 置为 true,然后跳出循环。如果循环结束都没有子 Viwe 处理事件则什么都不做。

  2. 接着判断如果 mFirstTouchTarget 为 null,这里会调用 ViewGroup 的 dispatchTransformedTouchEvent 方法,其中会调用父类的 dispatchTouchEvent 方法处理事件并将处理结果返回。有两种情况下 mFirstTouchTarget 为 null,第一种情况是如果事件是 ACTION_DOWN 并且遍历所有子 View 后没有子 View 处理事件从而导致 mFirstTouchTarget 为null;第二种情况是 ViewGroup 从 ACTION_DOWN 时就开始拦截事件所以没有遍历所有子 View 从而导致任何事件到来时执行到这里 mFirstTouchTarget 都为 null。

  3. 第 2 点可以说明如果 ViewGroup 在 ACTION_DOWN 事件时拦截,那么 ACTION_DOWN 事件 ViewGroup 会处理,并且同一事件系列中其他事件时,不管 ViewGroup 是否拦截这里都会调用 ViewGroup 的方法来处理事件。还可以说明 ACTION_DOWN 时如果 ViewGroup 不拦截但是所有子 View 没有处理事件这时 ViewGroup 会处理事件。这也是唯一一种将事件传递到子 View 后子 View 没处理但是 ViewGroup 会处理的情况

  4. 接着判断 mFirstTouchTarget 不为 null 时,会先判断子 View 已经处理了事件的标识是否为 true。因为只有 ACTION_DOWN 事件时且有子 View 处理了事件时才会在前面为该标记赋值为 true。如果为 true,说明这是 ACTION_DOWN 事件且 ViewGroup 不拦截并且遍历子 View 处理事件时有子 View 处理了事件,则将事件处理结果赋值 true 。如果该标记为 false,说明不是 ACTION_DOWN 事件。这时会根据前面部分的 ViewGroup 是否拦截此事件来判断,如果 ViewGroup 拦截则调用 ViewGroup 的方法处理事件并将处理结果返回,如果 ViewGroup 不拦截此事件,则由 mFirstTouchTarget 标记的 View 来处理事件,并将结果返回。

  5. 第 4 点可以看出,如果 ACTION_DOWN 事件被子 View 处理即 mFirstTouchTarget 不为 null 时,如果当前事件不是 ACTION_DOWN 且 ViewGroup 不拦截此事件,则会将事件传递到子 View 处理,然后不管子 View 是否处理了该事件, ViewGroup 都不会再处理,只会将处理结果返回到上一层,第 5 点与第 3 点是对比分析的。因为第 3 点中提到的情况是唯一一种将事件传递到子 View 后子 View 没有处理了事件但 ViewGroup 会处理的情况。

分析到这里,ViewGroup 的事件分发和拦截过程就基本结束了,所有问题都指向了一个方法 View.dispatchTouchEvent() ,不管是 ViewGroup 处理事件还是子 View 处理事件都会执行该方法,我们接下来就分析这个方法干了什么。

2. Viwe 的 dispatchTouchEvent() 方法

首先来看源码

// View
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        // ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { // 如果为 View 设置了 OnTouchListener 会首先调用 OnTouchListener.onTouch 方法
            result = true;
        }

        if (!result && onTouchEvent(event)) { // 如果 OnTouchListener.onTouch  方法返回 false ,则执行 onTouchEvent 方法
            result = true;
        }
    }

    ...
    return result;
}

// View
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    if ((viewFlags & ENABLED_MASK) == DISABLED) { // 如果 View 是 DISABLED 的,则直接返回该 View 是否可点击 CLICKABLE
        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);
    }
    
    // 如果设置了代理,类似 OnTouchListener ,则会调用代理的 onTouchEvent 方法,如果该方法返回 true ,则直接返回处理结果 true
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { // 只要 View 时候 CLICKABLE 或者 LONG_CLICKABLE 都会判断为 true
        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) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        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) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) { // PerformClick 为一个 Runnable 
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) { // 调用 performClick 方法,通过 post 方法和 Runnable 保证 performClick 执行在 UI 线程
                                performClick();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}

// View
public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo; // onClickListenenr
    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 的 dispatchTouchEvent 方法的源码,该方法会先判断是否设置了 OnTouchListener ,如果设置了则会调用 OnTouchListener 的 onTouch 方法,如果 onTouch 方法返回 true 则不会调用 onTouchEvent 方法,如果 onTouch 返回 false 或者没有设置 OnTouchListener 则会调用 onTouchEvent。这里看出 OnTouchListener.onTouch 方法调用的时机在 View 的 dispatchTouchEvent 方法之前,且 OnTouchListener.onTouch 的优先级是高于 onTouchEvent 方法的(也高于 onClickListener.onClick() 方法,接下来下面会分析)。

View 的 onTouchEvent() 方法中,根据 View 的是否可用即 Enable 或者 Disable 来判断,如果是 Disable ,那么之接返回该 View 是否可点击 CLICKABLE 或者支持 LONG_CLICKABLE,说明 Disable 不影响 View 是否消耗事件。

接下来,如果 View 设置了代理,类似 OnTouchListener ,则会调用代理的 onTouchEvent 方法,如果该方法返回 true ,则直接返回处理结果 true.

如果 View 是 Enable 的,且为 CLICKABLE 或者 LONG_CLICKABLE 或者 CONTEXT_CLICKABLE 的,则会真正调用 View 处理事件的方法,我们关注 ACTION_UP 事件,该事件中会通过 post 和 Runnable 来调用 performClick 方法,保证该方法执行在 UI 线程,performClick 中,如果 View 设置了 onClickListenenr 会调用 OnClickListenenr.onClick() 方法,该方法没有返回值,最后 onTouchEvent 的执行结果。从这里可以得出 OnClickListener.onClick() 方法的执行时机在 onTouchEvent 方法中且该事件为 ACITON_UP,由此可以得出 ouTouchEvent 的优先级高于 OnClickListenenr.onClick() 方法

OnTouchListener.onTouch() > onTouchEvent() > OnClickListener.onClick()

这里还有一点,就是有关 View 是否是可点击的,默认情况下所有 View 的 LONG_CLICKABLE 都为 false,而 CLICKABLE 属性则和具体 View 有关,确切的说是如果 View 是可点击的那么 CLICKABLE 默认是 true, 例如:Button。如果 View 是不可点击的,那么 CLICKABLE 默认为 false,例如:TextView。并且 View 的 setOnClickListener 和 setOnLongClickListener 方法都会将对应属性设置为 true。

到这里事件的基本内容就讲完了,从整个流程可以看出,事件的分发过程是隧道式的也就是事件是从最外层的 Activity 一层一层传递到 View 中,而事件的处理则是冒泡式的,是从 View 一层一层传递到 Activity

下面将展示根据事件分发的源码总结一些常见但是疑难的结论,并附加 AndroidStudio 的 Log,如果感兴趣的可以自己敲一遍试试看。这些结论并不是全部的,所有人都可以在开发过程中根据源码总结出自己的结论

3. 事件分发源码归纳总结

3.1. 如果 ViewGroup 拦截某事件,则不管是什么事件,都会调用 ViewGroup 的处理事件的方法来处理事件

3.2. View 开始处理事件时,如果不消耗 ACTION_DOWN 即 dispatchTouchEvent() 返回 false ,事件会返回给父 View 处理,并且同一系列事件都不会交给它处理

  • ViewGroup 和 View 对 ACTION_DOWN 不处理,则 ACTION_MOVE 和 ACTION_UP 均不会传递给 ViewGroup 和 View

  • 如果是 ViewGroup 消耗,则不会传递给 View

  • 从源码分析是因为,系列中其他事件到来时,ViewGroup 的 dispatchTouchEvent() 中 mFirstTouchTarget 为 null,默认拦截自己处理

    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 onTouchEvent
    11-29 09:15:03.720 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:15:03.720 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 onTouchEvent

3.3. ViewGroup 如果 拦截并消耗 了 ACTION_DOWN 事件,那么同一事件系列中其他事件会直接交给该 ViewGroup 处理,不会再调用该 ViewGroup 的 onInterceptTouchEvent() 方法

  • 源码分析:系列中其他事件到来时,ViewGroup 的 dispatchTouchEvent() 中 mFirstTouchTarget 为 null,默认拦截自己处理
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent

3.4. 如果是子 View 消耗了事件,除非 View 调用了父 View 的 requestDisallowInterceptTouchEvent 方法设置不让父 View 拦截,否则同一系列中其他事件来临时在其父 ViewGroup 中还是会判断是否拦截

  • 源码分析:即使 mFirstTouchTarget 不为 null,在事件到来时 ViewGroup 的 dispatchTouchEvent 方法中还是会调用 onInterceptTouchEvent 方法来判断是否拦截

  • 如果 ViewGroup 的 FLAG_DISALLOW_INTERCEPT 标记开启,则不会拦截事件

    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent

    -------------------我是分割线----------------
    
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent

3.5. 当 View 消耗了 ACTION_DOWN ,如果其父 Viwe 不拦截事件,那么同系列中的其他事件还会传递到 View ,这是即使 View 不消耗事件系列中其他事件,其父 View 的 onTouchEvent() 事件也不会被调用,事件会传递到 Activity 的 onTouchEvent() 方法处理

  • 因为在 View 消耗了 ACTION_DOWN 时, mFirstTouchTarget 被赋值,ViewGroup 不拦截情况下,同一系列中其他事件到来时还是会传递到该 View

  • 在 GroupView 不拦截事件时只有事件为 ACTION_DOWN 且所有子 View 都没有处理了事件时才调用其父类也就是本身 View 的 dispatchTouchEvent() 方法来处理事件

  • Activity 中只要 DecorView 的 dispatchTouchEvent() 方法返回 false 就会调用自己的 onTouchEvent() 方法处理事件。

    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent

3.6. 除 ACTION_DOWN 以外,子 View 可以通过设置 FLAG_DISALLOW_INTERCEPT 标记位来影响 GroupView 是否拦截事件

  • 前提是 ACTION_DOWN 时 ViewGroup 没有拦截,并且 View 消耗了 ACTION_DOWN 时的事件 mFirstTouchTarget 被赋值。该
    不为 null 时,在 ViewGroup 的 dispatchTouchEvent() 中会根据该标记位判断是否需要调用 onInterceptTouchEvent() 方法,如果该标记位为 true ,则不会调用 onInterceptTouchEvent() 方法,即不会拦截
    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 {
        intercepted = true;
    }

整个事件分发的流程还是比较清晰的,只有真正了解了源码,再遇到事件分发问题时才能得心应手,下面就开始本篇文章的第二个重点,滑动冲突问题的解决

二、View 的滑动冲突

在应用中加了滑动效果后,简单的滑动效果是不会有什么大问题的。不过要是添加复杂的滑动效果,或者滑动是嵌套的情况时,滑动冲突问题就出现了。这里会先说一下滑动冲突的类型,再根据事件分发的原理找到统一的滑动冲突问题的解决方式

1. 滑动冲突的种类

  1. 外部跟内部滑动的方向不一致,例如 ViewPager 嵌套 ListView 的情况,ViwePager 是左右滑动,ListView 是上下滑动,这样在有滑动事件时便会出现滑动冲突。当然 ViewPager 默认帮我们解决了滑动冲突

  2. 外部跟内部滑动方向一致,例如 ScrollView 嵌套 ListView 的情况,ScrollView 可以上下滑动,ListView 也可以上下滑动,这时候如果有滑动事件系统将不知道用户到底想滑动哪一层,会出现第二种滑动冲突

  3. 上面两种情况的嵌套,例如 QQ 的侧滑菜单,主页,联系人列表三个 View,侧滑菜单跟首页的 ViewPager 都可以左右滑动,联系人列表 ListView 和 ViewPager 也会产生滑动冲突

下面来看滑动冲突的处理规则

2. 滑动冲突的处理规则

对于上面提到的第一种滑动冲突,它的处理规则比较简单,当用户左右滑动时让外部的 View 拦截事件,当用户上下滑动时让内部的 View 拦截事件。也就是根据滑动的特征来解决滑动冲突。至于如何判断用户是左右滑动还是上下滑动,我们可以根据用户滑动过程中左右偏移量和上下偏移量的对比来确定,哪个方向的偏移量大判定为哪个方向的滑动。除了通过偏移量对比,还可以使用速度、滑动方向跟水平方向的夹角等来确定。确定了是哪个方向滑动就能决定让相应 View 来响应滑动事件

对于第二种和第三种冲突,我们不能通过速度、偏移量、夹角等来判断,但是一般可以在业务上找到突破点,比如业务上规定当处于某种状态时内部相应,当处于另一种状态时外部相应,这样就根据业务确定了相应的处理规则。有了相应处理规则就可以决定让相应的 View 来响应滑动事件

3. 滑动冲突的解决方式

上面提到了三种滑动冲突场景,并且根据每种场景都提出了相应的处理原则,当处理原则确定之后我们就可以找到一种不依赖具体滑动规则的通用解决办法,并且在每种冲突场景时修改有关滑动规则的处理逻辑即可。

解决方式主要有外部拦截法内部拦截法 两种,下面一一来介绍

外部拦截法

外部拦截法是指所有的事件都需要经过外部 ViewGroup 的判断,如果外部 ViewGroup 需要此事件就拦截,如果外部 ViewGroup 不需要此事件就不拦截,外部拦截法需要重写外部 ViewGroup 的 onInterceptTouchEvent 方法,在内部根据相应规则确定是否拦截即可。

    // MyViewGroup
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                return false;
            case MotionEvent.ACTION_MOVE:
                return intercept();
                break;
            case MotionEvent.ACTION_UP:
                return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

以上代码即为外部拦截法的模板代码,在 ACTION_DOWN 时外部 ViewGroup 的 onInterceptTouchEvent 方法必须返回 false,否则内部 View 不能接收到事件,这里注意,内部 View 处理 ACTION_DOWN 事件必须返回 true,否则将接收不到之后的事件,ACTION_DOWN 之后的事件 ViewGroup 都会进行是否拦截的判断,intercept() 方法用来判断是否需要拦截,如果根据处理规则判定为需要拦截就返回 true 然后外部 ViewGroup 来处理事件;如果处理规则判定为不需要拦截就返回 false,让内部 View 来处理事件

内部拦截法

内部拦截法是指所有的事件外部 ViewGroup 都不拦截,所有事件都传递给内部 View,内部 View 如果需要此事件就直接处理,否则就通过 requestDisallowInterceptTouchEvent 方法来让外部 ViewGoup 拦截事件,内部拦截法较外部拦截发稍微复杂一点

    // MyViewGroup
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN)
            return false;
        else return true;
    }


    // MyView
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;
            case MotionEvent.ACTION_MOVE:
                if (!intercept())
                    getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }

以上代码即为内部拦截法的模板代码,其中外部 ViewGroup 的 onInterceptTouchEvent 方法中除了 ACTION_DOWN 需要返回 false,其他的事件都必须返回 true,这样才能再内部 View 中通过 requestDisallowInterceptTouchEvent 方法来控制外部 ViewGoup 拦截事件

内部 View 中的 onInterceptTouchEvent 方法中 ACTION_DOWN 事件必须返回 true,且需要调用 requestDisallowInterceptTouchEvent 方法设置外部 ViewGoup 不拦截其他事件,当内部 View 不需要其他事件时再次调用 requestDisallowInterceptTouchEvent 方法设置外部 ViewGroup 拦截事件。

注意:一旦内部 View 设置外部 ViewGroup 拦截事件,那么同一事件序列中之后的事件都不会再到达内部 View

以上就是外部拦截法和内部拦截法的大体结构,其中外部拦截法比较简单,实现的功能也比较全,内部拦截法有一定的缺点,所以在使用时最好选择外部拦截法。

到这里 Android 事件分发和 View 的滑动冲突的内容就结束啦,看起来很简单的流程,居然写了整整五个小时又改了三个小时。尽量表达的清晰,也尽量将整个事件分发过程描述清楚。希望可以帮到大家。如果有问题可以留言我们来一起讨论。

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

推荐阅读更多精彩内容