Android View的滑动冲突解决方法

注意:

  1. 阅读本文需要了解《Android事件分发机制》
  2. 在此知识点,本人也有部分困惑尚未完全解决,也会在文中标出出来。

常见的滑动冲突场景及对应的处理规则

  1. 外部滑动方向和内部滑动方向不一致
    面对这种情况的滑动冲突,解决规则是:根据滑动是水平滑动还是竖直滑动来判断由谁来拦截事件。判断滑动方向的方法是:比较水平方向和竖直方向滑动距离的大小,或者滑动路径和水平方向的夹角,或者根据水平方向和竖直方向的速度差。
  2. 外部滑动方向和内部滑动方向一致
    这种情况的滑动冲突,无法根据滑动的角度判断,一般都是根据业务需要来进行判断。
  3. 上面两种情况的结合
    这种情况比较复杂,一般也需要从业务上找到突破点。

场景一

场景一:假如打算做个像ViewPager一样的效果,父容器如horizonal方向的LinearLayout一样,容纳了三个ListView。

外部拦截法

就是指点击事件都先经过父容器的拦截处理,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部完成相应的拦截即可。

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;                                 //注解1
                  if (!mScroller.isFinished()){                       //注解2
                        mScroller.abortAnimation();
                        intercepted = true;
                  }
                  break;
            case MotionEvent.ACTION_MOVE:                 //注解3
                  int deltaX = x - mLastXIntercept;
                  int deltaY = y - mLastYintercept;
                  if (Math.abs(deltaX) > Math.abs(deltaY)){     //在这里的if中加入是否拦截的判断
                        intercepted = true;
                  } else {
                        intercepted = false;
                  }
                  break;
            case MotionEvent.ACTION_UP:
                  intercepted = fasle;                       //注解4
                  break;
            default:
                  break;         
      }
      mLastXIntercept = x;
      mLastYintercept = y;
      
      return intercepted; 
}

注解1:
为什么要在ACTION_DOWN时,intercepted = false?因为如果在ACTION_DOWN时,intercepted == true,那么根据Android事件分发机制,后面的MOVE事件和UP都会无条件的交给父容器去处理。这样的话,事件永远无法传递给子View。
此处的困惑:如果仅仅在父容器中设置ACTION_DOWN时,intercepted = false还是不够的。intercepted = false,然后DOWN事件传递给了子View,按照Android事件分发机制,如果子View没有成功处理DOWN事件(即返回了false),最终还是会调用父容器的处理方法。如果这样的话,后面的MOVE事件和UP依然会无条件的交给父容器去处理。
注解2:
这个if内的语句,针对的是下面的情况:如果用户此时在进行父容器的滑动方向(这里是水平滑动),但是在水平滑动之前如果用户再迅速进行竖直滑动,就会导致界面在水平方向无法滑动到终点从而处于一种中间状态。为了避免这种情况,当水平滑动时,下一个序列的点击事件仍然交给父容器处理(哪怕竖直方向滑动距离大于水平方向滑动距离,此时仍然判定是水平滑动)。
注解3:
这一块代码是解决滑动冲突的关键。在MOVE事件中,判断这一滑动事件是水平滑动还是竖直滑动,如果是水平滑动,作为父容器就拦截事件,如果是水平滑动,就不拦截事件,交给子view去处理。
此处的困惑:如果在一系列的MOVE事件中,前部分是水平移动,后部分是竖直移动的,那怎么办?因为没有拦截DOWN事件,所有很有可能事件拦截过程中的mFirstTouchTarget != null,所以后部分的MOVE事件仍然要调用onInterceptTouchEvent(),此时,intercept = false;,那么接着交给子View处理?
如何解答这个困惑呢?有一个结论是:

一旦父容器开始拦截任何一个事件,那么后续的事件都会交给它来处理。

这个结论先记住吧,暂时还没有搞明白为什么会这样。
注解4:
如果父容器在UP事件中返回了true,就会导致子View无法接受到UP事件,这个时候子元素中的onClick事件就无法处罚法。同样的,

因为一旦父容器开始拦截任何一个事件,那么后续的事件都会交给它来处理,所以UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在UP时返回了false

但是依然没有弄明白为什么有这个结论。

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这种方法需要配合requestDisallowInterceptTouchEvent()方法才能正常工作。
第一步,修改父容器的onInterceptTouchEvent(),让其在DOWN事件返回false,其他情况下返回true。

//父容器内
public boolean onInterceptTouchEvent(MotionEvent event){
      int x = (int)event.getX();
      int y = (int)event.getY();
      int action = event.getAction();
      if (action == MotionEvent.ACTION_DOWN){                  //注解5
            mLastX = x;
            mLastY = y;
            if (!mScroller.isFinished()){
                  mScroller.abortAnimation()
                  return true;
            }
            return true;
      } else {
            return true;
      }
 }

注解5:
父容器拦截了除了DOWN事件以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
第二步,修改子元素的dispatchTouchEvent()方法

//在子View中
public boolean dispatchTouchEvent(MotionEvent event){
      int x = (int) event.getX();
      int y = (int) event.getY();
      switch(event.getAction()){
      case MotionEvent.ACTION_DOWN:
            XXX.requestDisallowInterceptTouchEvent(true);
            break;
      case MotionEvent.ACTION_MOVE:
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (Math.abs(deltaX) > Math.abs(deltaY)){                              //在这里的if中加入是否拦截的判断
                  XXX.requestDisallowInterceptTouchEvent(false);
            }
            break;
      case MotionEvent.ACTION_UP:
            break;
      default:
            break;
      }
      mLastX = x;
      mLastY = y;
      return super.dispatchTouchEvent(event);               //注解6
}

注解6:因为子view是自定义view,重写的dispatchTouchEvent()方法,在解决了滑动冲突后,调用父类的dispatchTouchEvent()方法来进行原来的事件分发。

场景二和场景三

在总体的实现方法和场景一是一样的,仅仅是在MOVE事件中判断的条件不一样,场景一仅仅是通过滑动方向来进行判断,而场景二和场景三需要判断业务逻辑。这里就不详细介绍了。

你可能感兴趣的:(Android View的滑动冲突解决方法)