美文网首页自定义view
事件冲突原理和解决方法

事件冲突原理和解决方法

作者: w达不溜w | 来源:发表于2022-03-01 12:28 被阅读0次
    1、MotionEvent事件类型
    • ACTION_DOWN:手指初次触摸屏幕时触发
    • ACTION_MOVE:手指在屏幕上滑动时触发,会多次触发
    • ACTION_UP:手指离开屏幕时触发
    • ACTION_CANCEL:事件被上层拦截时触发
    2、事件分发流程
      Activity#dispatchTouchEvent()
    > PhoneWindow#superDispatchTouchEvent()
    > DecorView#superDispatchTouchEvent()
    > ViewGroup#dispatchTouchEvent()
    > View#dispatchTouchEvent()
    > View#onTouchEvent()
    
    3、onTouch和onClick冲突

    当我们setOnTouchListener且在onTouch()返回true表示事件被消费, setOnClickListener的onClick不会执行

    //View.java
    public boolean dispatchTouchEvent(MotionEvent event) {
      //...
      boolean result = false;
      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;
    }
    
    public boolean onTouchEvent(MotionEvent event) {
      switch (action) {
        case MotionEvent.ACTION_UP:
          performClickInternal();
          break;
        //...
      }
      //...
    }
    
    private boolean performClickInternal() {
      return performClick();
    }
    
    public boolean performClick() {
      final boolean result;
      final ListenerInfo li = mListenerInfo;
      if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        //回调onClick
        li.mOnClickListener.onClick(this);
        result = true;
      } else {
        result = false;
      }
      return result;
    }
    

    我们设置了setOnClickListener/setOnTouchListener,mListenerInfo就不会为空,如果li.mOnTouchListener.onTouch(this, event)为true,则result为true,!result短路就不会执行onTouchEvent(event)方法,而这个方法在ACTION_UP中回调onClick。

    4、DOWN事件分析

    事件都是以DOWN开始,UP事件结束,中间多个MOVE事件,所以我们先从DOWN事件开始分析

    //ViewGroup.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      //...
      if (actionMasked == MotionEvent.ACTION_DOWN) {
        // due to an app switch, ANR, or some other state change.
        cancelAndClearTouchTargets(ev);
        //①重置mGroupFlags
        resetTouchState();
      }
    
      // Check for interception.
      final boolean intercepted;
      if (actionMasked == MotionEvent.ACTION_DOWN
          || mFirstTouchTarget != null) {
        //②判断事件是否拦截
        //计算:mGroupFlags&~FLAG_DISALLOW_INTERCEPT&FLAG_DISALLOW_INTERCEPT=0
        //所以disallowIntercept为false,会执行onInterceptTouchEvent来判断是否拦截
        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事件进来,会先重置mGroupFlags变量,计算得到disallowIntercept恒为false,进入onInterceptTouchEvent来判断是否拦截。

    看下requestDisallowInterceptTouchEvent,表示请求父类不要拦截事件

    //ViewGroup.java
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
      if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
      }
    
      if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
      } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
      }
    
      // Pass it up to our parent
      if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
      }
    }
    

    当View设置requestDisallowInterceptTouchEvent为true的时候,mGroupFlags=mGroupFlags|FLAG_DISALLOW_INTERCEPT, 使得 disallowIntercept=mGroupFlags|FLAG_DISALLOW_INTERCEPT & FLAG_DISALLOW_INTERCEPT != 0,计算得到disallowIntercept恒为true,不会执行onInterceptTouchEvent,父View也不会拦截子View。

    但是当前事件是DOWN事件,requestDisallowInterceptTouchEvent是无效的。会调用resetTouchState重置mGroupFlags使得disallowIntercept为false,进入onInterceptTouchEvent来判断是否拦截

    1)未被拦截:intercepted为false,事件分发给子View处理

    //事件分发流程
    if (!canceled && !intercepted) {
      // Scan children from front to back.
      //对子View进行排序
      final ArrayList<View> preorderedList = buildTouchDispatchChildList();
      for (int i = childrenCount - 1; i >= 0; i--) {
        //遍历拿到child
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        //判断child的位置是否是点击事件
         if (!child.canReceivePointerEvents()
             || !isTransformedTouchPointInView(x, y, child, null)) {
           ev.setTargetAccessibilityFocus(false);
           //不符合取下一个child
           continue;
         }
         //分发给子View处理事件
         if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssiif(){
           //如果子View处理了事件,会执行addTouchTarget,mFirstTouchTarget就会被赋值
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
           //alreadyDispatchedToNewTouchTarget赋值为true
           alreadyDispatchedToNewTouchTarget = true;
           break; 
         }
      } 
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
       final boolean handled;
       if (child == null) {
         handled = super.dispatchTouchEvent(transformedEvent);
       } else {
         //child去处理事件
         handled = child.dispatchTouchEvent(transformedEvent);
       }
       return handled;
     }
    

    2)被拦截:intercepted为true,会跳过if (!canceled && !intercepted)

    if (mFirstTouchTarget == null) {
      // No touch targets so treat this as an ordinary view.
      //当事件为down 我们拦截事件时候
      handled = dispatchTransformedTouchEvent(ev, canceled, null,
          TouchTarget.ALL_POINTER_IDS);
    } else {
      // Dispatch to touch targets, excluding the new touch target if we already
      // dispatched to it.  Cancel touch targets if necessary.
      //onInterceptTouchEvent返回false
      TouchTarget predecessor = null;
      TouchTarget target = mFirstTouchTarget;
      while (target != null) {
        //down事件next为空,具体处理在addTouchTarget方法中
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
          ////down事件这两个条件都满足,到此就结束了
          handled = true;
        } else {
          //move事件进来alreadyDispatchedToNewTouchTarget会被设置为false
          final boolean cancelChild = resetCancelNextUpFlag(target.child)
            || intercepted;
          //dispatchTransformedTouchEvent会调到childView里去,所以parentView滑动无效
          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,调用dispatchTransformedTouchEvent,传入的child为null,调用super.dispatchTouchEvent(transformedEvent)进入View的dispatchTouchEvent方法。

    完成了DOWN事件分发和处理后,再次分发MOVE事件,还是从ViewGroup的dispatchTouchEvent方法开始,而MOVE事件是可以反复调用此方法的,MOVE事件不进行分发,直接找DOWN事件确定的对象。

    5、冲突解决方式

    1)外部拦截法 (在ViewGroup中对事件进行拦截)
    重写parentView中的onInterceptTouchEvent

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
      switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
          break;
        }
        case MotionEvent.ACTION_MOVE: {
          if (parentView需要此事件) {
            //进行拦截
            return true;
          }
          break;
        }
        case MotionEvent.ACTION_UP: {
          break;
        }
        default:
          break;
      }
      return super.onInterceptTouchEvent(event);
    }
    

    2)内部拦截法(ViewGroup不拦截,子View需要事件就消耗掉,否则交给父View处理)
    重写parentView的onInterceptTouchEvent

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
      //不拦截down事件
      if (event.getAction() == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(event);
        return false;
      }
      return true;
    }
    

    重写childView的dispatchTouchEvent

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
      switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
          getParent().requestDisallowInterceptTouchEvent(true);
          break;
        case MotionEvent.ACTION_MOVE:
          if(parentView需要此事件){
            getParent().requestDisallowInterceptTouchEvent(false);
          }
          break;
        case MotionEvent.ACTION_UP:
          break;
      }
      return super.dispatchTouchEvent(event);
    }
    

    相关文章

      网友评论

        本文标题:事件冲突原理和解决方法

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