View的事件分发机制以及滑动冲突

2016-10-12  本文已影响186人  人失格

View的事件分发机制以及滑动冲突

[TOC]

点击事件的传递规则

点击时间的分发过程 总是绕不过三个很重要的方法来共同完成:dispatchTouchEvent(MotionEvent ev), onIntercepTouchEvent(MotionEvent ev), onTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent ev)

​ 用来进行事件的分发。如果时间能够分发到当前View,那么此方法一定会被调用,返回的结果受View的OntouchEvent和下级View的dispatchEvent方法的影响,表示是否消耗当前事件。

public boolean onIntercepTouchEvent(MotionEvent ev)

​ 只有ViewGroup才会拥有的方法,用于拦截某个事件,如果当前的View拦截某个事件,那么在同一个时间序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

​ **public boolean onTouchEvent(MotionEvent ev) **

​ 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。

那么这三个方法的调用顺序是如何呢?

public boolean dispatchTouchEvent(MotionEvent ev){
  boolean consume = false;
  if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev); 
  }else{
    consume = child.dispatchTouchEvent(ev);
  }
  return consume;
}

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,此时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,及它的onTouchEvent方法就会被调用,如果它的onInterceptTouchEvent返回为false,表示它不拦截当前事件,此时当前事件就会继续传递给它的子元素,此时如果是View,则会直接调用onTouchEvent方法。

OnTouchListener, View.onTouchEvent 和OnclickListener的区别

当一个View需要处理事件。设置了OnTouchListener,则OnTouchListener的onTouch方法会被回调,如果onTouch的方法返回True,则View.onTouchEvent方法不会被调用,反之则会被调用。View.onTouchEvent方法中,如果当前设置的有OnclickListener,其优先级最低。

这三者的优先级: OnTouchListener -> View.onTouchEvent -> OnclickListener

当一个点击事件产生后,它的传递过程遵循如下顺序 Activity -> PhoneWindow -> RootView。 由Activity 传给PhoneWindow Window 最后传给顶级View。

关于时间传递的机制,我们首先在这里给一些结论,

  1. 同一事件顺序是指手指接触屏幕的那一刻起, 到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个时间序列以Down事件开始,中间含有数量不一的move事件。最后以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗,因此一个时间序列的事件不能分别由两个View同时处理,但是我们可以通过代码控制事件传递。
  3. 某个View一旦决定拦截,那么一个事件序列都只能由它来处理,并且它的onIntercepTouchEvent不会再被调用。当一个View决定拦截一个事件后,同一事件的剩下事件也会交给它来处理,也就是说onIntercepTouchEvent不会被再调用。
  4. 某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件(OnTouchEvent返回为false),那么同一事件中的其他时间都不会再交给它来处理,并且事件将重新交由它的父元素去处理。
  5. 如果View不消耗除了ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理
  6. ViewGroup默认是不拦截任何事件,默认onInterceptTouchEvent方法默认返回False
  7. View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
  8. View的OntouchEvent默认都是会消耗事件,除非它是不可以点击的(clickable longclickable同时为false)。 View的longClickable属性都是false,clickable属性要分情况,Button的clickable属性默认为true,TextView的clickable属性默认为false。 当然 如果给view设置了setOnclickListener 或者setOnLongClickListener 会默认开启。
  9. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素中干涉父元素的事件分发过程,但是ACTION_DOWN事件除外。

事件分发的源码分析

点击事件是有MotionEvent来表示,当一个点击事件发生,最先传入的是Activity,由Activity的disPatchEvent来进行事件派发,,具体工作是由Activity内部的Window来完成的。 Window会将时间传递给DecorView,一般DecorView就是当前界面的顶级容器(即是setContentView所设置的View的父容器),如图:

public boolean disPatchTouchEvent(MotionEvent ev){
  if(ev.getAction == MotionEvent.ACTION_DOWN){
        onUserInteraction();
  }
  if(getWindow().superDispatchTouchEvent(ev)){
    return truel
  }
  return onTouchEvent(ev);
}

Window是如何将事件传递给ViewGroup的呢,Window类其实是一个抽象类 它可以控制顶级view的外观和行为策略,Window的唯一实现是PhoneView类,

publlic boolean superDisPatchTouchEvent(MotionEvent event){
  return mDecor.superDispatchTouchEvent(event);
}

这个mDecot其实就是我们getWindow().getDecorView()返回的View,我们通过设置setContentView设置的View就是它的一个子View,自此事件传递到了顶级View,即我们设置的SetContentView所设置的View。

ViewGroup的事件分发机制

我们在看一下ViewGroup对点击事件的分发过程,主要实现在disPatchEvent方法中,

final boolean intercepted
if(actionMasked == MotionEvent.Action_Down || mFirsrtTouchTarget != null){
  final boolean disallowIntercept = (GroupFlag & FLAG_DISALLOW_INTERCEPT) != 0;
  if(!disallowIntercept){
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action);
  }else{
    intercepted = false;
  } 
}else{
  intercepted = true;
}

由ViewGroup的子元素成功处理时, mFirsrtTouchTarget 会被赋值并指向子元素。所以一旦事件是由当前的ViewGroup拦截,接下来同一时序的其他事件都会默认交给ViewGroup来处理。

另外一个特殊情况就是FLAG_DISALLOW_INTERCEPT标识符,这这个一般通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View,一般设置ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,因为ViewGroup在事件分发的时候会重置FLAG_DISALLOW_INTERCEPT标识符,这意味着当面对ACTION_DOWN的时候,ViewGroup一定会调用onInterceptTouchEvent来判断自己是否需要拦截事件

                 final ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);                                                                           
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))                            {                           
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }

首先遍历所有的ViewGroup的所有子元素,判断是否能够接受到点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,dispatchTransformedTouchEvent就是调用了child的dispatchTouchEvent方法, 如果子元素的disPatchTouchEvent返回false 则会继续分发给下一个子元素(如果有的话), 在addTouchTarget(child, idBitsToAssign) 给mFirsrtTouchTarget 赋值。mFirsrtTouchTarget 是否为null直接影响ViewGroup对事件的拦截策略。

            dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
            {
                if (child == null) {
                handled = super.dispatchTouchEvent(event);
                } else {
                handled = child.dispatchTouchEvent(event);
                }
            }

如果遍历了所有的子元素事件都没有被合适处理,这包含两种情况: 第一种是ViewGroup没有子元素,第二种是子元素处理了点击事件,但是在dispatchTouchEvenr中返回了false 一般是子元素在OnTouchEvent中返回了false,这是ViewGroup会自己处理点击事件。

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

View的事件分发

         if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

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

首先判断是否有设置OnTouchListener,如果onTouchListener中的Touch返回true,那么onTouchEvent就不会被调用。

                if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                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 (!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)) {
                                    performClick();
                                }
                            }
                        }
                   

只要View的CLICKABLE或者是LONG_CLICKABLE 那么他就会消耗这件事,当ACTION_UP事件发生 会触发performClick() 如果设置了OnclickListener ,performClick就是调用Onclick方法。

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

设置Viewd的OnclickListener和OnLongClickListener()会自动将View的Clickable,LongClickAble为true。

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    
     public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

View的滑动冲突

常见的滑动冲突场景

对应处理的方法:

  1. 当用户左右滑动时,需要外部拦截点击事件,当用户需要上下滑动时,需要让内部拦截点击事件
  2. 这种场景无法根据滑动的角度, 距离差已经速度差来做判断 一般需要从业务上找到突破口
  3. 同上,一般也是从业务的需要上得出相应的处理规则

两种滑动冲突的解决方案:

  1. 外部拦截法

    所谓的外部拦截法就是所有的点击事件都先经过父容器的拦截处理,如果父容器需要此事件则拦截 如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制,外部拦截法需要重写父容器的onInterceptTouchEvent方法。伪代码如下

     
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            int action = ev.getAction();
            float y = ev.getY();
    
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dy = y - mLastY;
                   if(父容器需要当前点击事件){
                     return true;
                   }
                    break;
            }
           // 默认返回的都是false 
            return super.onInterceptTouchEvent(ev);
        }
    
    1. 内部拦截法

      内部拦截法主要是父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗,否则交由父容器进行处理,需要配合requestDisallowInterceptTouchEvent()才能正常工作,一般内部拦截法比较的复杂,它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

      
        @Override
          public boolean dispatchTouchEvent(MotionEvent ev) {
              int action = ev.getAction();
              float y = ev.getY();
      
              switch (action) {
                  case MotionEvent.ACTION_DOWN:
                    parent.requestDisallowInterceptTouchEvent(true)
                      break;
                  case MotionEvent.ACTION_MOVE:              
                     if(父容器需要当前点击事件){
                       return parent.requestDisallowInterceptTouchEvent(false);
                     }
                      break;
              }
             // 默认返回的都是false 
              return super.dispatchTouchEvent(ev);
          }
      
      

      面对不同的滑动策越的时候只需要修改ACTION_MOVE事件即可,其他不需要动也不能改动。除了子元素需要处理外,父元素也要默认拦截除了ACTION_DOWN以外的所有其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)时,父元素才能继续拦截所需要的事件。

上一篇下一篇

猜你喜欢

热点阅读