前言 上一篇讲了Android触摸事件的传递机制,具体可以看这里  初识Android触摸事件传递机制 。既然知道Android中触摸事件的传递分发,那么它能解决什么样的问题,在我们实际开发中如何应用,这点很重要,知道原理是为了解决问题而准备的。这篇文章的核心讲的如何解决View的滑动冲突,这个问题在日常开发中很常见,比如内部嵌套Fragment视图是左右滑动,外部用一个ScrollView来包含,可以上下滑动,如果不进行滑动冲突处理的话,就会造成外部滑动方向和内部滑动方向不一致。
目录 
常见的滑动冲突场景 
滑动冲突的处理规则 
外部拦截法 
内部拦截法 
小结 
 
常见的滑动冲突场景 常见的滑动冲突场景可以简单分为以下三种:
场景1:外部滑动方向和内部滑动方向不一致 
场景2:外部滑动方向和内部滑动方向一致 
场景3:上面两种情况的嵌套 
 
如图:
场景1,主要是将ViewPager和Fragment配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果。在这个效果中可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView,所以就造成了滑动冲突,但是在ViewPager内部处理了这种滑动冲突,因此在采用ViewPager时我们就无须关注这个问题,而如果把ViewPager换成ScrollView,那就必须自己手动处理,不然造成的结果就是内外两层只能一层能够滑动。
场景2,就复杂一点,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层滑动,要么就是内外两层都滑动但很卡顿。
场景3,是场景1和场景2两种情况的嵌套,显得更复杂了。比如外部有一个SlideMenu效果,内部有一个ViewPager,ViewPager的每一个页面中又是一个ListView。虽然场景3滑动冲突看起来很复杂,但都是几个单一的滑动冲突的叠加,因此需要一一拆解开来即可。
滑动冲突的处理规则 一般来说,不管滑动冲突有多么复杂,它都有既定的规则,根据这些规则我们就可以选择合适的方法去处理。
对于场景1,它的处理规则就是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动,需要让内部View拦截点击事件。具体来说就是根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。
如图:
简单来说,就是根据水平方向和竖直方向的距离差来判断,如果是Dx>Dy,那么则是水平滑动,如果是Dy>Dx,那么则是竖直滑动。
场景2,则是比较特殊,它无法根据滑动的角度,距离差以及速度差来做判断。这个时候就需要从业务上找到突破点,比如,当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时需要内部View来响应View的滑动
对于场景3的话,它的滑动规则也更复杂,和场景2一样,同样是从业务上找到突破点。
外部拦截法 外部拦截法是指点击事件都是先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件,就不拦截了,这样就可以解决滑动冲突的问题,外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Override   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;   } 
首先ACTION_DOWN这个事件,父容器必须返回false,这样保证后续move和up的事件可以传递给子View,根据move事件来决定是否拦截,如果父容器拦截就返回true,否则返回false。
实现一个自定义类似ViewPager的控件,嵌套ListView的效果,源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 public  class  HorizontalScrollViewEx  extends  ViewGroup  {    private  static  final  String  TAG  =  "HorizontalScrollViewEx" ;     private  int  mChildrenSize;     private  int  mChildWidth;     private  int  mChildIndex;          private  int  mLastX  =  0 ;     private  int  mLastY  =  0 ;          private  int  mLastXIntercept  =  0 ;     private  int  mLastYIntercept  =  0 ;     private  Scroller mScroller;                     private  VelocityTracker mVelocityTracker;       public  HorizontalScrollViewEx (Context context)  {         super (context);         init();     }     public  HorizontalScrollViewEx (Context context, AttributeSet attrs)  {         super (context, attrs);         init();     }     public  HorizontalScrollViewEx (Context context, AttributeSet attrs,              int  defStyle)  {        super (context, attrs, defStyle);         init();     }     private  void  init ()  {         mScroller = new  Scroller (getContext());         mVelocityTracker = VelocityTracker.obtain();     }     @Override      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 ;             if  (!mScroller.isFinished()) {                 mScroller.abortAnimation();                 intercepted = true ;             }             break ;         }         case  MotionEvent.ACTION_MOVE: {             int  deltaX  =  x - mLastXIntercept;             int  deltaY  =  y - mLastYIntercept;             if  (Math.abs(deltaX) > Math.abs(deltaY)) {                 intercepted = true ;             } else  {                 intercepted = false ;             }             break ;         }         case  MotionEvent.ACTION_UP: {             intercepted = false ;             break ;         }         default :             break ;         }         Log.d(TAG, "intercepted="  + intercepted);         mLastX = x;         mLastY = y;         mLastXIntercept = x;         mLastYIntercept = y;         return  intercepted;     }     @Override      public  boolean  onTouchEvent (MotionEvent event)  {         mVelocityTracker.addMovement(event);         int  x  =  (int ) event.getX();         int  y  =  (int ) event.getY();         switch  (event.getAction()) {         case  MotionEvent.ACTION_DOWN: {             if  (!mScroller.isFinished()) {                 mScroller.abortAnimation();             }             break ;         }         case  MotionEvent.ACTION_MOVE: {             int  deltaX  =  x - mLastX;             scrollBy(-deltaX, 0 );             break ;         }         case  MotionEvent.ACTION_UP: {             int  scrollX  =  getScrollX();             mVelocityTracker.computeCurrentVelocity(1000 );             float  xVelocity  =  mVelocityTracker.getXVelocity();             if  (Math.abs(xVelocity) >= 50 ) {                 mChildIndex = xVelocity > 0  ? mChildIndex - 1  : mChildIndex + 1 ;             } else  {                 mChildIndex = (scrollX + mChildWidth / 2 ) / mChildWidth;             }             mChildIndex = Math.max(0 , Math.min(mChildIndex, mChildrenSize - 1 ));             int  dx  =  mChildIndex * mChildWidth - scrollX;             smoothScrollBy(dx, 0 );             mVelocityTracker.clear();             break ;         }         default :             break ;         }         mLastX = x;         mLastY = y;         return  true ;     }     @Override      protected  void  onMeasure (int  widthMeasureSpec, int  heightMeasureSpec)  {         super .onMeasure(widthMeasureSpec, heightMeasureSpec);         int  measuredWidth  =  0 ;         int  measuredHeight  =  0 ;         final  int  childCount  =  getChildCount();         measureChildren(widthMeasureSpec, heightMeasureSpec);         int  widthSpaceSize  =  MeasureSpec.getSize(widthMeasureSpec);         int  widthSpecMode  =  MeasureSpec.getMode(widthMeasureSpec);         int  heightSpaceSize  =  MeasureSpec.getSize(heightMeasureSpec);         int  heightSpecMode  =  MeasureSpec.getMode(heightMeasureSpec);         if  (childCount == 0 ) {             setMeasuredDimension(0 , 0 );         } else  if  (heightSpecMode == MeasureSpec.AT_MOST) {             final  View  childView  =  getChildAt(0 );             measuredHeight = childView.getMeasuredHeight();             setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());         } else  if  (widthSpecMode == MeasureSpec.AT_MOST) {             final  View  childView  =  getChildAt(0 );             measuredWidth = childView.getMeasuredWidth() * childCount;             setMeasuredDimension(measuredWidth, heightSpaceSize);         } else  {             final  View  childView  =  getChildAt(0 );             measuredWidth = childView.getMeasuredWidth() * childCount;             measuredHeight = childView.getMeasuredHeight();             setMeasuredDimension(measuredWidth, measuredHeight);         }     }     @Override      protected  void  onLayout (boolean  changed, int  l, int  t, int  r, int  b)  {         int  childLeft  =  0 ;         final  int  childCount  =  getChildCount();         mChildrenSize = childCount;         for  (int  i  =  0 ; i < childCount; i++) {             final  View  childView  =  getChildAt(i);             if  (childView.getVisibility() != View.GONE) {                 final  int  childWidth  =  childView.getMeasuredWidth();                 mChildWidth = childWidth;                 childView.layout(childLeft, 0 , childLeft + childWidth,                         childView.getMeasuredHeight());                 childLeft += childWidth;             }         }     }     private  void  smoothScrollBy (int  dx, int  dy)  {         mScroller.startScroll(getScrollX(), 0 , dx, 0 , 500 );         invalidate();     }     @Override      public  void  computeScroll ()  {         if  (mScroller.computeScrollOffset()) {             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());             postInvalidate();         }     }     @Override      protected  void  onDetachedFromWindow ()  {         mVelocityTracker.recycle();         super .onDetachedFromWindow();     } } 
这个情况的拦截条件就是父容器在滑动过程中水平距离差比垂直距离差大,那么就进行拦截,否则就不拦截,继续传递事件。
内部拦截法 内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法复杂。伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override  public  boolean  dispatchTouchEvent (MotionEvent event)  {      int  x  =  (int ) event.getX();      int  y  =  (int ) event.getY();      switch  (event.getAction()) {      case  MotionEvent.ACTION_DOWN: {          mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true );          break ;      }      case  MotionEvent.ACTION_MOVE: {          int  deltaX  =  x - mLastX;          int  deltaY  =  y - mLastY;          if  (父容器需要此类点击事件) {              mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false );          }          break ;      }      case  MotionEvent.ACTION_UP: {          break ;      }      default :          break ;      }      mLastX = x;      mLastY = y;      return  super .dispatchTouchEvent(event);  } 
当子元素调用requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
前面是用自定义类似的ViewPager,现在重写一个ListView,我们可以自定义一个ListView,叫做ListViewEx,然后对内部拦截法的模板代码进行修改即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public  class  ListViewEx  extends  ListView  {    private  static  final  String  TAG  =  "ListViewEx" ;     private  HorizontalScrollViewEx2 mHorizontalScrollViewEx2;          private  int  mLastX  =  0 ;     private  int  mLastY  =  0 ;     public  ListViewEx (Context context)  {         super (context);     }     public  ListViewEx (Context context, AttributeSet attrs)  {         super (context, attrs);     }     public  ListViewEx (Context context, AttributeSet attrs, int  defStyle)  {         super (context, attrs, defStyle);     }     public  void  setHorizontalScrollViewEx2 (              HorizontalScrollViewEx2 horizontalScrollViewEx2)  {        mHorizontalScrollViewEx2 = horizontalScrollViewEx2;     }     @Override      public  boolean  dispatchTouchEvent (MotionEvent event)  {         int  x  =  (int ) event.getX();         int  y  =  (int ) event.getY();         switch  (event.getAction()) {         case  MotionEvent.ACTION_DOWN: {             mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true );             break ;         }         case  MotionEvent.ACTION_MOVE: {             int  deltaX  =  x - mLastX;             int  deltaY  =  y - mLastY;             Log.d(TAG, "dx:"  + deltaX + " dy:"  + deltaY);             if  (Math.abs(deltaX) > Math.abs(deltaY)) {                 mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false );             }             break ;         }         case  MotionEvent.ACTION_UP: {             break ;         }         default :             break ;         }         mLastX = x;         mLastY = y;         return  super .dispatchTouchEvent(event);     } } 
同时对于包含ListViewEx外部布局进行修改,在onInterceptTouchEvent事件上不进行拦截
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 public  class  HorizontalScrollViewEx2  extends  ViewGroup  {    private  static  final  String  TAG  =  "HorizontalScrollViewEx2" ;     private  int  mChildrenSize;     private  int  mChildWidth;     private  int  mChildIndex;          private  int  mLastX  =  0 ;     private  int  mLastY  =  0 ;          private  int  mLastXIntercept  =  0 ;     private  int  mLastYIntercept  =  0 ;     private  Scroller mScroller;     private  VelocityTracker mVelocityTracker;     public  HorizontalScrollViewEx2 (Context context)  {         super (context);         init();     }     public  HorizontalScrollViewEx2 (Context context, AttributeSet attrs)  {         super (context, attrs);         init();     }     public  HorizontalScrollViewEx2 (Context context, AttributeSet attrs,              int  defStyle)  {        super (context, attrs, defStyle);         init();     }     private  void  init ()  {         mScroller = new  Scroller (getContext());         mVelocityTracker = VelocityTracker.obtain();     }     @Override      public  boolean  onInterceptTouchEvent (MotionEvent event)  {         int  x  =  (int ) event.getX();         int  y  =  (int ) event.getY();         int  action  =  event.getAction();         if  (action == MotionEvent.ACTION_DOWN) {             mLastX = x;             mLastY = y;             if  (!mScroller.isFinished()) {                 mScroller.abortAnimation();                 return  true ;             }             return  false ;         } else  {             return  true ;         }     }     @Override      public  boolean  onTouchEvent (MotionEvent event)  {         Log.d(TAG, "onTouchEvent action:"  + event.getAction());         mVelocityTracker.addMovement(event);         int  x  =  (int ) event.getX();         int  y  =  (int ) event.getY();         switch  (event.getAction()) {         case  MotionEvent.ACTION_DOWN: {             if  (!mScroller.isFinished()) {                 mScroller.abortAnimation();             }             break ;         }         case  MotionEvent.ACTION_MOVE: {             int  deltaX  =  x - mLastX;             int  deltaY  =  y - mLastY;             Log.d(TAG, "move, deltaX:"  + deltaX + " deltaY:"  + deltaY);             scrollBy(-deltaX, 0 );             break ;         }         case  MotionEvent.ACTION_UP: {             int  scrollX  =  getScrollX();             int  scrollToChildIndex  =  scrollX / mChildWidth;             Log.d(TAG, "current index:"  + scrollToChildIndex);             mVelocityTracker.computeCurrentVelocity(1000 );             float  xVelocity  =  mVelocityTracker.getXVelocity();             if  (Math.abs(xVelocity) >= 50 ) {                 mChildIndex = xVelocity > 0  ? mChildIndex - 1  : mChildIndex + 1 ;             } else  {                 mChildIndex = (scrollX + mChildWidth / 2 ) / mChildWidth;             }             mChildIndex = Math.max(0 , Math.min(mChildIndex, mChildrenSize - 1 ));             int  dx  =  mChildIndex * mChildWidth - scrollX;             smoothScrollBy(dx, 0 );             mVelocityTracker.clear();             Log.d(TAG, "index:"  + scrollToChildIndex + " dx:"  + dx);             break ;         }         default :             break ;         }         mLastX = x;         mLastY = y;         return  true ;     }     @Override      protected  void  onMeasure (int  widthMeasureSpec, int  heightMeasureSpec)  {         super .onMeasure(widthMeasureSpec, heightMeasureSpec);         int  measuredWidth  =  0 ;         int  measuredHeight  =  0 ;         final  int  childCount  =  getChildCount();         measureChildren(widthMeasureSpec, heightMeasureSpec);         int  widthSpaceSize  =  MeasureSpec.getSize(widthMeasureSpec);         int  widthSpecMode  =  MeasureSpec.getMode(widthMeasureSpec);         int  heightSpaceSize  =  MeasureSpec.getSize(heightMeasureSpec);         int  heightSpecMode  =  MeasureSpec.getMode(heightMeasureSpec);         if  (childCount == 0 ) {             setMeasuredDimension(0 , 0 );         } else  if  (heightSpecMode == MeasureSpec.AT_MOST) {             final  View  childView  =  getChildAt(0 );             measuredHeight = childView.getMeasuredHeight();             setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());         } else  if  (widthSpecMode == MeasureSpec.AT_MOST) {             final  View  childView  =  getChildAt(0 );             measuredWidth = childView.getMeasuredWidth() * childCount;             setMeasuredDimension(measuredWidth, heightSpaceSize);         } else  {             final  View  childView  =  getChildAt(0 );             measuredWidth = childView.getMeasuredWidth() * childCount;             measuredHeight = childView.getMeasuredHeight();             setMeasuredDimension(measuredWidth, measuredHeight);         }     }     @Override      protected  void  onLayout (boolean  changed, int  l, int  t, int  r, int  b)  {         Log.d(TAG, "width:"  + getWidth());         int  childLeft  =  0 ;         final  int  childCount  =  getChildCount();         mChildrenSize = childCount;         for  (int  i  =  0 ; i < childCount; i++) {             final  View  childView  =  getChildAt(i);             if  (childView.getVisibility() != View.GONE) {                 final  int  childWidth  =  childView.getMeasuredWidth();                 mChildWidth = childWidth;                 childView.layout(childLeft, 0 , childLeft + childWidth,                         childView.getMeasuredHeight());                 childLeft += childWidth;             }         }     }     private  void  smoothScrollBy (int  dx, int  dy)  {         mScroller.startScroll(getScrollX(), 0 , dx, 0 , 500 );         invalidate();     }     @Override      public  void  computeScroll ()  {         if  (mScroller.computeScrollOffset()) {             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());             postInvalidate();         }     }     @Override      protected  void  onDetachedFromWindow ()  {         mVelocityTracker.recycle();         super .onDetachedFromWindow();     } } 
这个拦截规则也是父容器在滑动过程中水平距离差与垂直距离差相比。
小结 总的来说,滑动冲突的场景可以分为三种,内外部方向不一致、内外部方向一致、嵌套前面两种情况。如何解决,不管多么复杂的滑动冲突,可以进行拆分,根据的一定的规则,第一种情况可根据滑动距离差、速度差和角度差来解决,第二种和第三种情况,可根据业务上找到突破点,业务上一种状态需要响应,切换到另外一种状态时则不响应,根据业务需求得出相应的处理规则,有了处理规则可以进行下一步处理。