美文网首页源码分析
Android 事件冲突处理

Android 事件冲突处理

作者: 昊空_6f4f | 来源:发表于2020-07-14 10:38 被阅读0次

    概述

    本文主要分享Android常见的事件冲突处理,处理方式有两种:

    • 外部拦截:父容器处理冲突
    • 内部拦截:子控件处理冲突

    在介绍这两种处理方法之前,我们必须先了解两件事情:

    • 事件在控件中是如何传递的
    • 事件冲突产生的根本原因

    事件在控件中是如何传递的

    先来看一张事件分发的大致流程图:

    通过流程图可知,事件的分发是从Activity的dispatchTouchEvent开始传递的,然后调用PhoneWindow的superDispatchTouchEvent,再调用DecorView的superDispatchTouchEvent,再调用到ViewGroup的dispatchTouchEvent方法,ViewGroup要先走分发流程,再走处理流程,而View只能走处理流程。下面便从ViewGroup的dispatchTouchEvent方法分析事件的传递流程。

    DOWN事件

    事件的分发是从Down事件开始的,Down事件只有一个,ViewGroup的dispatchTouchEvent方法对Down事件的处理方式有以下两种:

    • 拦截事件
    • 不拦截事件

    接下来结合源码分析这两种处理方式有什么区别。

    拦截事件

    跟踪ViewGroup中dispatchTouchEvent方法针对ACTION_DOWN处理的关键代码:

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            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 (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        }
    

    上述中有一句关键代码:
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    disallowIntercept表示是否不允许父控件拦截,由于在MotionEvent.ACTION_DOWN中调用了resetTouchState方法:

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    

    所以在MotionEvent.ACTION_DOWN时disallowIntercept的值为fasle,此时会调用ViewGroup的onInterceptTouchEvent,因为拦截了Down事件,所以onInterceptTouchEvent返回true,此时事件停止向子控件分发,交给自身处理即mFirstTouchTarget==null,然后会调用到以下的关键代码:
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)

    跟踪dispatchTransformedTouchEvent方法的关键代码:

    if (child == null) {
          handled = super.dispatchTouchEvent(transformedEvent);
      } else {
          final float offsetX = mScrollX - child.mLeft;
          final float offsetY = mScrollY - child.mTop;
          transformedEvent.offsetLocation(offsetX, offsetY);
          if (! child.hasIdentityMatrix()) {
              transformedEvent.transform(child.getInverseMatrix());
          }
      
          handled = child.dispatchTouchEvent(transformedEvent);
      }
    

    由源码可知,由于child==null会调用到 super.dispatchTouchEvent方法,即调用到View的dispatchTouchEvent方法,关键代码如下:

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

    由源码可知,View的dispatchTouchEvent中会根据mOnTouchListener.onTouch或onTouchEvent是否返回true,来判断是否消费该事件。到这里拦截事件的基本流程就结束了。
    这里补充一个小知识点,由于mOnTouchListener.onTouch是优先与onTouchEvent,所以当mOnTouchListener.onTouch返回true时,以下代码不会执行:

    if (!result && onTouchEvent(event)) {
              result = true;
      }
    

    那如果此时控件同时设置了onClick事件便会失效,因为在onTouchEvent的ACTION_UP事件中调用了 performClick() 方法:

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

    不拦截事件

    ViewGroup的onInterceptTouchEvent返回false(默认返回false)时,此时会将事件分发给子控件处理,如果子控件都不处理则自己处理该事件,关键代码如下:

        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
                   
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
        
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;
        
                removePointersFromTouchTargets(idBitsToAssign);
        
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
        
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }
        
                        if (!child.canReceivePointerEvents()
                                || !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)) {
                            // 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);
                            alreadyDispatchedToNewTouchTarget = true;
                            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 (preorderedList != null) preorderedList.clear();
                }
        
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }
    

    遍历子控件集合时,会根据子控件的dispatchTransformedTouchEvent方法判断是否有子控件处理了事件,若有子控件处理,会执行如关键代码:

        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    这里采用了链表来存储目标控件,此时mFirstTouchTarget不为空,那如果子控件都没有处理,是如何将事件再交给父控件处理呢?继续跟踪源码:

    if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            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;
            }
        }
    

    当没有子控件都没有处理事件时,mFirstTouchTarget=null,此时会调用ViewGroup的dispatchTransformedTouchEvent方法自己处理该事件,如果有子控件处理,会执行以下判断:

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

    因为有子控件处理,此时alreadyDispatchedToNewTouchTarget = true、mFirstTouchTarget=newTouchTarget、 target.next=null即满足上述条件,while循环只会执行一次,到这里不拦截事件的基本流程就结束了。

    MOVE事件

    Move事件的传递也是通过以下两种方式进行分析:

    • 不拦截Move事件传递
    • 拦截Move事件传递

    Move事件正常传递

    父控件不拦截事件时,Move事件的传递也是通过dispatchTouchEvent方法传递给目标控件的,关键代码如下:

        boolean alreadyDispatchedToNewTouchTarget = false;
        View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                            ? findChildWithAccessibilityFocus() : null;
    
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                            .....
                    }//此时是Move事件,不会执行这段代码
        }
        .....
        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;
        }
    

    这里需要注意由于此时alreadyDispatchedToNewTouchTarget=false,所以会走else分支,会执行以下关键代码:

     if (dispatchTransformedTouchEvent(ev, cancelChild,
            target.child, target.pointerIdBits)) {
        handled = true;
    }
    

    将Move事件交给对应的目标控件(Down事件保存的Target),到这里正常的Move事件就执行完了。

    拦截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;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }
    

    通过上面Down事件分析可知,由于Down事件做了重置操作,所以disallowIntercept的值为false,即if分支的代码一定会执行,此时拦截子控件的Move事件,会执行以下关键代码:

        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                        || intercepted;
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            handled = true;
        }
    
        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;
            }
    }
    

    因为拦截了move事件,此时intercepted=true, cancelChild=true,此时会设置子控件的Action为MotionEvent.ACTION_CANCEL,取消子控件的事件,并且注意以下代码:

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

    由于此时cancelChild=true, mFirstTouchTarget被设置成null,本次Move事件就结束了,注意ViewGroup是在下一Move事件才能够接收到事件,因为下一次Move事件会重新走dispatchTouchEvent方法,关注以下代码:

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

    由于此时是Move事件并mFirstTouchTarget=null,所以此时走else分支intercepted = true,Move事件会交给自身处理,关联代码:

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

    小结一下,当父控件拦截Move事件时,第一次会将子控件的事件类型设置为MotionEvent.ACTION_CANCEL并将mFirstTouchTarget赋值为null,此时第一次Move事件结束(由于子控件的dispatchTransformedTouchEvent返回true),第二次以后的Move事件才会传递到父控件。

    UP与Cancel事件

    一次完整的事件,首先有Down事件开始,中间有多个Move事件,最后由Up/Cancel事件结束,Up事件是正常结束,而Cancel事件是被父控件拦截后产生的

    事件分发完整流程图

    为了进一步理解上述的源码分析流程,下面提供一张完整的事件分发流程图:

    896629-20171007002836974-997068426.png

    事件冲突处理

    通过前面的铺垫,可以知道事件冲突只能在Move事件中处理,可以通过外部拦截和内部拦截处理事件冲突,这里以SwipeRefreshLayout嵌套ViewPager为例:

    外部拦截

    根据父控件的滑动逻辑在onInterceptTouchEvent方法中返回true/false,核心代码:

    public class CustomSRL2 extends SwipeRefreshLayout {
    
        //外部拦截成员变量
        private float startX;
        private float startY;
        //ViewPager是否滚动
        boolean mIsVpMove = false;
        //触发移动事件的最小距离,如果小于这个距离就不触发移动控件,如Viewpager就是用这个距离来判断用户是否翻页
        private int mTouchSlop;
    
        public CustomSRL2(Context context) {
            this(context, null);
        }
    
        public CustomSRL2(Context context, AttributeSet attrs) {
            super(context, attrs);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
    
        //外部拦截
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = ev.getX();
                    startY = ev.getY();
                    mIsVpMove = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //若此时ViewPager还在滑动,则返回false,不拦截
                    if (mIsVpMove) {
                        return false;
                    }
                    float x = ev.getX();
                    float y = ev.getY();
                    float deltaX = Math.abs(x - startX);
                    float deltaY = Math.abs(y - startY);
                    if (deltaX > mTouchSlop && deltaX > deltaY) {
                        mIsVpMove = true;
                        return false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mIsVpMove = false;
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    }
    

    内部拦截

    根据子控件的滑动逻辑调用父控的requestDisallowInterceptTouchEvent(true/false)方法通知父控件是否不拦截事件,核心代码:

    public class CustomSRL2 extends SwipeRefreshLayout {
    
        public CustomSRL2(Context context) {
            super(context);
        }
    
        public CustomSRL2(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        //以下代码为内部拦截代码
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            //在ACTION_DOWN事件返回false,不拦截事件,将事件交给子控件处理
            if(ev.getAction() == MotionEvent.ACTION_DOWN){
                super.onInterceptTouchEvent(ev);
                return false;
            }
            return true;//拦截事件
        }
    }
    
    public class CustomVPInner extends ViewPager {
    
        private float startX;
        private float startY;
    
        public CustomVPInner(Context context) {
            super(context);
        }
    
        public CustomVPInner(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        //内部拦截:使用ViewCompat.setNestedScrollingEnabled(this,true),参考以下代码
        /**
         * public void requestDisallowInterceptTouchEvent(boolean b) {
         *         // if this is a List < L or another view that doesn't support nested
         *         // scrolling, ignore this request so that the vertical scroll event
         *         // isn't stolen
         *         if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
         *                 || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
         *             // Nope.
         *         } else {
         *             super.requestDisallowInterceptTouchEvent(b);
         *         }
         *     }
         */
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = ev.getX();
                    startY = ev.getY();
                    ViewCompat.setNestedScrollingEnabled(this,true);
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float x = ev.getX();
                    float y = ev.getY();
                    float deltaX = Math.abs(x - startX);
                    float deltaY = Math.abs(y - startY);
                    if (deltaX < deltaY) {
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
            }
    
            //打印ViewPager是否消费了该事件,如果没有,事件还是会交给SwipeRefreshLayout处理
            boolean consume = super.dispatchTouchEvent(ev);
            Log.e("fmt","consume=" + consume);
    
            return super.dispatchTouchEvent(ev);
        }
    }
    

    完整代码实现

    百度链接
    密码:1cq9

    相关文章

      网友评论

        本文标题:Android 事件冲突处理

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