美文网首页Android开发Android进阶开发Android开发经验谈
Android进阶知识:事件分发与滑动冲突(二)

Android进阶知识:事件分发与滑动冲突(二)

作者: 881ef7b85f62 | 来源:发表于2019-04-24 16:28 被阅读23次

    接着看代码块3,在这段很长的代码里,首先一个if中判断了该事件是否满足没有被拦截和被取消,之后第二个if判断了事件类型是否为DOWN,满足了没有被拦截和取消的DOWN事件,接下来ViewGroup才会循环其子View找到点击事件在其内部并且能够接受该事件的子View,再通过调用dispatchTransformedTouchEvent方法将事件分发给该子View处理,返回true说明子View成功消费事件,于是调用addTouchTarget方法,方法中通过TouchTarget.obtain方法获得一个包含这View的TouchTarget节点并将其添加到链表头,并将已经分发的标记设置为true
    接下来看代码块4:

                // Dispatch to touch targets.
                //走到这里说明在循环遍历所有子View后没有找到接受该事件或者事件不是DOWN事件或者该事件已被拦截或取消  
                if (mFirstTouchTarget == null) {
                    //mFirstTouchTarget为空说明没有子View响应消费该事件
                    //所有调用dispatchTransformedTouchEvent方法分发事件
                    //注意这里第三个参数传的是null,方法里会调用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                     // mFirstTouchTarget不为空说明有子View能响应消费该事件,消费过之前的DOWN事件,就将这个事件还分发给这个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 (target != null) {
                        final TouchTarget next = target.next;
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            handled = true;
                        } else {
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                                //这里传入的是target.child就是之前响应消费的View,把该事件还交给它处理
                            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;
                    }
                }
    

    之前在代码块3中处理分发了没被拦截和取消的DOWN事件,那么其他MOVEUP等类型事件怎么处理呢?还有如果遍历完子View却没有能接受这个事件的View又怎么处理呢?代码块4中就处理分发了这些事件。首先判断mFirstTouchTarget是否为空,为空说明没有子View消费该事件,于是就调用dispatchTransformedTouchEvent方法分发事件,这里注意dispatchTransformedTouchEvent方法第三个参数View传的null,方法里会对于这种没有子View能处理消费事件的情况,就调用该ViewGroup的super.dispatchTouchEvent方法,即View的dispatchTouchEvent,把ViewGroup当成View来处理,把事件交给ViewGroup处理。具体看dispatchTransformedTouchEvent方法中的这段代码:

                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
    

    dispatchTransformedTouchEvent方法中child即传入的View为空则调用super.dispatchTouchEvent方法分发事件,就是View类的分发方法,不为空则调用子View方法,即child.dispatchTouchEvent分发事件,所以归根结底都是调用了View类的dispatchTouchEvent方法处理。

    至此,ViewGroup中的分发过流程结束,再来总结一下这个过程:首先过滤掉不安全的事件,接着如果事件类型是DOWN事件认为是一个新的事件序列开始,就清空TouchTarget链表重置相关标志位(代码块一),然后判断是否拦截该事件,这里有两步判断:一是如果是DOWN事件或者不是DOWN事件但是mFirstTouchTarget不等于null(这里mFirstTouchTarget如果等于null说明之前没有View消费DOWN事件,在代码块三末尾,可以看到如果有子View消费了DOWN事件,会调用addTouchTarget方法,获得一个保存了该子View的TouchTarget,并将其添加到mFirstTouchTarget链表头),则进入第二步禁止拦截标记的判断,否则直接设置为需要拦截,进入第二步判断设置过禁止拦截标记为true的就不拦截,否则调用ViewGroup的onInterceptTouchEvent方法根据返回接过来决定是否拦截(代码块二)。接下来如果事件没被拦截也没被取消而且还是DOWN事件,就循环遍历ViewGroup中的子View找到事件在其范围内并且能接受事件的子View,通过dispatchTransformedTouchEvent方法将事件分发给该子View,然后通过addTouchTarget方法将包含该子View的TouchTarget插到链表头(代码块三)。最后如果没有找到能够接受该事件的子View又或者是MOVEUP类型事件等再判断mFirstTouchTarget是否为空,为空说明之前没有View能接受消费该事件,则调用dispatchTransformedTouchEvent方法将事件交给自身处理,不为空则同样调用dispatchTransformedTouchEvent方法,但是是将事件分发给该子View处理。

    ViewGroup的onInterceptTouchEvent方法:
    public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                    && ev.getAction() == MotionEvent.ACTION_DOWN
                    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
                return true;
            }
            return false;
        }
    

    在ViewGroup的dispatchTouchEvent中没设置过禁止拦截的事件默认都会通过onInterceptTouchEvent方法来决定是否拦截,onInterceptTouchEvent方法里可以看到默认是返回false,只有在事件源类型是鼠标并且是DOWN事件是鼠标点击按钮和是滚动条的手势时才返回true。所以默认一般ViewGroup的onInterceptTouchEvent方法返回都为false,也就是说默认不拦截事件。

    ViewGroup的onTouchEvent方法:

    ViewGroup中没有覆盖onTouchEvent方法,所以调用ViewGroup的onTouchEvent方法实际上调用的还是它的父类View的onTouchEvent方法。

    View的dispatchTouchEvent方法:

    在ViewGroup中将事件无论是分发给子View的时候还是自己处理的,最终都会执行默认的View类的dispatchTouchEvent方法:

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

    这里同样省略一些代码只看关键的,首先同样和ViewGroup一样,做了事件安全性的过滤,接着先判断了mOnTouchListener是否为空,不为空并且该View是ENABLED可用的,就会调用mOnTouchListeneronTouch方法,如果onTouch方法返回true说明事件已经被消费了,就将result标记修改为true,这样他就不会走接下来的if了。如果没有设置mOnTouchListener或者onTouch方法返回false,则会继续调用onTouchEvent方法。这里可以发现mOnTouchListeneronTouch方法的优先级是在onTouchEvent之前的,如果在代码中设置了mOnTouchListener监听,并且onTouch返回true,则这个事件就被在onTouch里消费了,不会在调用onTouchEvent方法。

    //这个mOnTouchListener就是经常在代码里设置的View.OnTouchListener
    mMyView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    //这里返回true事件就消费了,不会再调用onTouchEvent方法
                    return true;
                }
            });
    
    View的onTouchEvent方法:
     public boolean onTouchEvent(MotionEvent event) {
     /---------------代码块-1-------------------------------------------------------------------
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
    /---------------代码块-1------完-------------------------------------------------------------   
    /---------------代码块-2-------------------------------------------------------------------
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    /---------------代码块-2------完-------------------------------------------------------------   
    /---------------代码块-3-------------------------------------------------------------------    
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        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) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        //调用了OnClickListener
                                        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:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        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, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    /---------------代码块-3------完-------------------------------------------------------------   
            return false;
        }
    

    onTouchEvent方法里的代码也不少,不过大部分都是响应事件的一些逻辑,与事件分发流程关系不大。还是分成三块,先看第一个代码块:

        final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
            //这里CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一个满足,clickable就为true
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
            //这里先判断当前View是否可用,如果是不可用进入if代码块
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
            //如果是UP事件并且View处于PRESSED状态,则调用setPressed设置为false
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                //这里如果View是不可用状态,就直接返回clickable状态,不做任何处理
                return clickable;
            }
    

    代码块1中首先获得View是否可点击clickable,然后判断View如果是不可用状态就直接返回clickable,但是没做任何响应。View默认的clickablefalseEnabledture,不同的View的clickable默认值也不同,Button默认clickabletrueTextView默认为false

            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    

    代码块2中会对一个mTouchDelegate触摸代理进行判断,不为空会调用代理的onTouchEvent响应事件并且返回true

     if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        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) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        //调用了OnClickListener
                                        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:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        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, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    

    代码块3中首先判断了 clickable || (viewFlags & TOOLTIP) == TOOLTIP 满足了这个条件就返回true消费事件。接下来的switch中主要对事件四种状态分别做了处理。这里稍微看下在UP事件中会调用一个performClick方法,方法中调用了OnClickListeneronClick方法。

    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);
            notifyEnterOrExitForAutoFillIfNeeded(true);
            return result;
        }
    

    最后看到onTouchEvent的最后一行默认返回的还是false,就是说只有满足上述的条件之一才会返回ture
    至此事件分发的相关源码就梳理完了,我画了几张流程图,能更清新的理解源码逻辑。

    ViewGroup的dispatchTouchEvent逻辑:

    View的dispathTouchEvent逻辑:

    事件分发整体逻辑

    4、事件分发机制相关问题

    阅读了源码之后,先来解决之前提到的三个问题。

    Q1:为什么日志Demo中只有ACTION_DOWN事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为什么ACTION_MOVEACTION_UP事件没有?

    A1:日志Demo代码所有事件传递方法都是默认调用super父类对应方法,所以根据源码逻辑可知当事件序列中的第一个DOWN事件来临时,会按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView的顺序分发,ViewGroup中onInterceptTouchEvent方法默认返回false不会拦截事件,最终会找到合适的子View(这里即MyView)dispatchTransformedTouchEvent方法,将事件交给子View的dispatchTouchEvent处理,在dispatchTouchEvent方法中默认会调用View的onTouchEvent方法处理事件,这里因为MyView是继承View的,所以默认clickablefalse,而onTouchEvent方法中当clickablefalse时默认返回的也是false。最终导致ViewGroup中dispatchTransformedTouchEvent方法返回为false。进而导致mFirstTouchTarget为空,所以后续MOVEUP事件到来时,因为mFirstTouchTarget为空,事件拦截标记直接设置为true事件被拦截,就不会继续向下分发,最终事件无人消费就返回到Activity的onTouchEvent方法。所以就会出现这样的日志输出。

     if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {
                        intercepted = onInterceptTouchEvent(ev);
                        ev.setAction(action);  
                    } else {
                        intercepted = false;
                    }
                } else {
                    //mFirstTouchTarget为空intercepted为true且不会调用onInterceptTouchEvent方法
                    intercepted = true;
                }
    
    Q2:为什么将设置clickable="true"之后ACTION_MOVEACTION_UP事件就会执行了?

    A2:如A1中所说,clickable设置为true,View的onTouchEvent方法的返回就会为true,消费了DOWN事件,就会创建一个TouchTarget插到单链表头,mFirstTouchTarget就不会是空了,MOVEUP事件到来时,就会由之前消费了DOWN事件的View来处理消费MOVEUP事件。

    Q3:requestDisallowInterceptTouchEvent方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent方法也不执行了?

    A3:源码阅读是有看到,requestDisallowInterceptTouchEvent方法时通过位运算设置标志位,在调用传入参数为true后,事件在分发时disallowIntercept会为true!disallowIntercept即为false,导致事件拦截标记interceptedfalse,不会进行事件拦截。

    Q4:View.OnClickListeneronClick方法与View.OnTouchListeneronTouch执行顺序?

    A4::View.OnClickListeneronClick方法是在View的onTouchEventperformClick方法中调用的。 而View.OnTouchListeneronTouch方法在View的dispatchTouchEvent方法中看到是比onTouchEvent方法优先级高的,并且只要OnTouchListener.Touch返回为true,就只会调用OnTouchListener.onTouch方法不会再调用onTouchEvent方法。所以View.OnClickListeneronClick方法顺序是在View.OnTouchListeneronTouch之后的。

    5、滑动冲突

    关于滑动冲突,在《Android开发艺术探索》中有详细说明,我这里把书上的方法结论与具体实例结合起来做一个总结。

    1.滑动冲突的场景

    常见的场景有三种:

    • 外部滑动与内部滑动方向不一致
    • 外部滑动与内部滑动方向一致
    • 前两种情况的嵌套
    2.滑动冲突的处理规则

    不同的场景有不同的处理规则,例如上面的场景一,规则一般就是当左右滑动时,外部View拦截事件,当上下滑动时要让内部View拦截事件,这时候处理滑动冲突就可以根据滑动是水平滑动还是垂直滑动来判断谁来拦截事件。场景而这种同个方向上的滑动冲突一般要根据业务逻辑来处理规则,什么时候要外部View拦截,什么时候要内部View拦截。场景三就更加复杂了,但是同样是根据具体业务逻辑,来判断具体的滑动规则。

    推荐阅读:终于有人把 【移动开发】 从基础到实战的全套视频弄全了

    3.滑动冲突的解决方法
    • 外部拦截法
    • 内部拦截法

    外部拦截法是从父View着手,所有事件都要经过父View的分发和拦截,什么时候父View需要事件,就将其拦截,不需要就不拦截,通过重写父View的onInterceptTouchEvent方法来实现拦截规则。

        private int mLastXIntercept;
        private int mLastYIntercept;
        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;
        }
    

    按照以上伪代码,根据不同的拦截要求进行修改就可以解决滑动冲突。

    内部拦截法的思想是父View不拦截事件,由子View来决定事件拦截,如果子View需要此事件就直接消耗掉,如果不需要就交给父View处理。这种方法需要配合requestDisallowInterceptTouchEvent方法来实现。

    private int  mLastX;
    private int  mLastY;
    @Override
     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方法
      @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
         int action = event.getAction();
         if (action == MotionEvent.ACTION_DOWN) {
             return false;
         } else {
             return true;
         }
     }  
    

    这里父View不拦截ACTION_DOWN方法的原因,根据之前的源码阅读可知如果ACTION_DOWN事件被拦截,之后的所有事件就都不会再传递下去了。

    4.滑动冲突实例

    实例一:ScrollView与ListView嵌套
    这个实例是同向滑动冲突,先看布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <cScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ScrollDemo1Activity">
    
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
    
            <com.example.sy.eventdemo.MyView
                android:layout_width="match_parent"
                android:layout_height="350dp"
                android:background="#27A3F3"
                android:clickable="true" />
    
            <ListView
                android:id="@+id/lv"
                android:layout_width="match_parent"
                android:background="#E5F327"
                android:layout_height="300dp"></ListView>
    
            <com.example.sy.eventdemo.MyView
                android:layout_width="match_parent"
                android:layout_height="500dp"
                android:background="#0AEC2E"
                android:clickable="true" />
        </LinearLayout>
    </cScrollView>
    

    这里MyView就是之前打印日志的View没有做任何其他处理,用于占位使ScrollView超出一屏可以滑动。
    运行效果:

    可以看到ScrollView与ListView发生滑动冲突,ListView的滑动事件没有触发。接着来解决这个问题,用内部拦截法。

    首先自定义ScrollView,重写他的onInterceptTouchEvent方法,拦击除了DOWN事件以外的事件。

    public class MyScrollView extends ScrollView {
    
        public MyScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onTouchEvent(ev);
                return false;
            }
            return true;
        }
    
    }
    

    这里没有拦截DOWN事件,所以DOWN事件无法进入ScrollView的onTouchEvent事件,又因为ScrollView的滚动需要在onTouchEvent方法中做一些准备,所以这里手动调用一次。接着再自定义一个ListView,来决定事件拦截,重写dispatchTouchEvent方法。

    package com.example.sy.eventdemo;
    
    import android.content.Context;
    import android.os.Build;
    import android.support.annotation.RequiresApi;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.widget.ListView;
    
    /**
     * Create by SY on 2019/4/22
     */
    public class MyListView extends ListView {
        public MyListView(Context context) {
            super(context);
        }
    
        public MyListView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
        private float lastY;
    
        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                getParent().getParent().requestDisallowInterceptTouchEvent(true);
            } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
                if (lastY > ev.getY()) {
                    // 这里判断是向上滑动,而且不能再向上滑了,说明到头了,就让父View处理
                    if (!canScrollList(1)) {
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                        return false;
                    }
                } else if (ev.getY() > lastY) {
                    // 这里判断是向下滑动,而且不能再向下滑了,说明到头了,同样让父View处理
                    if (!canScrollList(-1)) {
                        getParent().getParent().requestDisallowInterceptTouchEvent(false);
                        return false;
                    }
                }
            }
            lastY = ev.getY();
            return super.dispatchTouchEvent(ev);
        }
    }
    

    判断是向上滑动还是向下滑动,是否滑动到头了,如果滑到头了就让父View拦截事件由父View处理,否则就由自己处理。将布局文件中的空间更换。

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ScrollDemo1Activity">
    
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
    
            <com.example.sy.eventdemo.MyView
                android:layout_width="match_parent"
                android:layout_height="350dp"
                android:background="#27A3F3"
                android:clickable="true" />
    
            <com.example.sy.eventdemo.MyListView
                android:id="@+id/lv"
                android:layout_width="match_parent"
                android:background="#E5F327"
                android:layout_height="300dp"></com.example.sy.eventdemo.MyListView>
    
            <com.example.sy.eventdemo.MyView
                android:layout_width="match_parent"
                android:layout_height="500dp"
                android:background="#0AEC2E"
                android:clickable="true" />
        </LinearLayout>
    </com.example.sy.eventdemo.MyScrollView>
    

    运行结果:

    实例二:ViewPager与ListView嵌套
    这个例子是水平和垂直滑动冲突。使用V4包中的ViewPager与ListView嵌套并不会发生冲突,是因为在ViewPager中已经实现了关于滑动冲突的处理代码,所以这里自定义一个简单的ViewPager来测试冲突。布局文件里就一个ViewPager:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ScrollDemo2Activity">
    
        <com.example.sy.eventdemo.MyViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
    </LinearLayout>
    

    ViewPager的每个页面的布局也很简单就是一个ListView:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ScrollDemo2Activity">
    
        <com.example.sy.eventdemo.MyViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
    
    </LinearLayout>
    

    开始没有处理滑动冲突的运行效果是这样的:

    看到现在只能上下滑动响应ListView的滑动事件,接着我们外部拦截发解决滑动冲突,核心代码如下:

        case MotionEvent.ACTION_MOVE:
                    int gapX = x - lastX;
                    int gapY = y - lastY;
                    //当水平滑动距离大于垂直滑动距离,让父view拦截事件
                    if (Math.abs(gapX) > Math.abs(gapY)) {
                        intercept = true;
                    } else {
                        //否则不拦截事件
                        intercept = false;
                    }
                    break;
    

    onInterceptTouchEvent中当水平滑动距离大于垂直滑动距离,让父view拦截事件,反之父View不拦截事件,让子View处理。
    运行结果:

    这下冲突就解决了。这两个例子分别对应了上面的场景一和场景二,关于场景三的解决方法其实也是一样,都是根据具体需求判断事件需要由谁来响应消费,然后重写对应方法将事件拦截或者取消拦截即可,这里就不再具体举例了。

    6、总结

    • Android事件分发顺序:Activity-->ViewGroup-->View
    • Android事件响应顺序:View-->ViewGroup-->Activity
    • 滑动冲突解决,关键在于找到拦截规则,根据操作习惯或者业务逻辑确定拦截规则,根据规则重新对应拦截方法即可。
    Android高级架构脑图详细地址

    关于FAndroid进阶知识的全部学习内容,我们这边都有系统的知识体系以及进阶视频资料,有需要的朋友可以加群免费领取安卓进阶视频教程,源码,面试资料,群内有大牛一起交流讨论技术;点击链接加入群聊【腾讯@Android高级架构】(包括自定义控件、NDK、架构设计、混合式开发工程师(React native,Weex)、性能优化、完整商业项目开发等)

    Android高级进阶视频教程

    相关文章

      网友评论

        本文标题:Android进阶知识:事件分发与滑动冲突(二)

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