浅谈Android View滑动冲突
引言
上一篇文章我们从源码的角度介绍了View事件分发机制,这一篇文章我们就通过介绍滑动冲突的规则和一个实例来更加深入的学习View的事件分发机制。
1、外部滑动方向和内部滑动方向不一致
考虑这样一种场景,开发中我们经常使用ViewPager和Fragment配合使用所组成的页面滑动效果,很多主流的应用都会使用这样的效果。在这种效果中,可以使用左右滑动来切换界面,而每一个界面里面往往又都是ListView这样的控件。本来这种情况是存在滑动冲突的,只是ViewPager内部处理了这种滑动冲突。如果我们不使用ViewPager而是使用ScrollView,那么滑动冲突就需要我们自己来处理,否者造成的后果就是内外两层只有一层能滑动。
情况1的解决思路
对于第一种情况的解决思路是这样的:当用户左右滑动时,需要让外层的View拦截点击事件。当用户上下滑动时,需要让内部的View拦截点击事件(外层的View不拦截点击事件),这时候我们就可以根据它们的特性来解决滑动冲突。在这里我们可以根据滑动时水平滑动还是垂直滑动来判断谁来拦截点击事件。下面先介绍一种通用的解决滑动冲突的方法。
外部拦截法
外部拦截法是指:点击事件都经过父容器的拦截处理,如果父容器需要处理此事件就进行拦截,否者不拦截交给子View进行处理。这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。这种方法的伪代码如下:
1 @Override 2 public boolean onInterceptTouchEvent(MotionEvent ev) { 3 int x=(int)ev.getX(); 4 int y=(int)ev.getY(); 5 boolean intercept=false; 6 switch (ev.getAction()){ 7 //按下事件不要拦截,否则后续事件都会给ViewGroup处理 8 case MotionEvent.ACTION_DOWN: 9 intercept=false; 10 break; 11 case MotionEvent.ACTION_MOVE: 12 //如果是横向移动就进行拦截,否则不拦截 13 int deltaX=x-mLastX; 14 int deltaY=y-mLastY; 15 if(父容器需要当前点击事件){ 16 intercept=true; 17 }else { 18 intercept=false; 19 } 20 break; 21 case MotionEvent.ACTION_UP: 22 intercept=false; 23 break; 24 } 25 mLastX = x; 26 mLastY = y; 27 return intercept; 28 }
上面代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件的条件即可,其他均不需要修改。我们在描述下:在onInterceptTouchEvent方法中,首先是ACTION_DOWN事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP都会直接交给父容器处理,这时候事件就没法传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否需要拦截。
下面来看一个具体的实例,这个实现模拟ViewPager的效果,我们定义一个全新的控件,名称叫HorizontalScrollView。具体代码如下:
1、我们先看Activity中的代码:
1 public class MainActivity extends Activity{ 2 3 private HorizontalScrollView mListContainer; 4 5 @Override 6 protected void onCreate(Bundle savedInstanceState) { 7 super.onCreate(savedInstanceState); 8 setContentView(R.layout.activity_main); 9 10 initView(); 11 } 12 13 private void initView() { 14 LayoutInflater inflater = getLayoutInflater(); 15 mListContainer = (HorizontalScrollView) findViewById(R.id.container); 16 final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels; 17 for (int i = 0; i < 3; i++) { 18 ViewGroup layout = (ViewGroup) inflater.inflate( 19 R.layout.content_layout, mListContainer, false); 20 layout.getLayoutParams().width = screenWidth; 21 TextView textView = (TextView) layout.findViewById(R.id.title); 22 textView.setText("page " + (i + 1)); 23 layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0)); 24 createList(layout); 25 mListContainer.addView(layout); 26 } 27 } 28 29 private void createList(ViewGroup layout) { 30 ListView listView = (ListView) layout.findViewById(R.id.list); 31 ArrayList<String> datas = new ArrayList<>(); 32 for (int i = 0; i < 50; i++) { 33 datas.add("name " + i); 34 } 35 36 ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, datas); 37 listView.setAdapter(adapter); 38 } 39 }
在这个代码中,我们创建了3个ListView然后将其添加到我们自定义控件的。这里HorizontalScrollView是父容器,ListView是子View。下面我们就使用外部拦截法来实现HorizontalScrollView,代码如下:
1 /** 2 * 横向布局控件 3 * 模拟经典滑动冲突 4 * 我们此处使用ScrollView来模拟ViewPager,那么必须手动处理滑动冲突,否则内外两层只能有一层滑动,那就是滑动冲突。另外内部左右滑动,外部上下滑动也同样属于该类 5 */ 6 public class HorizontalScrollView extends ViewGroup { 7 8 //记录上次滑动的坐标 9 private int mLastX = 0; 10 private int mLastY = 0; 11 private WindowManager wm; 12 //子View的个数 13 private int mChildCount; 14 private int mScreenWidth; 15 //自定义控件横向宽度 16 private int mMeasureWidth; 17 //滑动加载下一个界面的阈值 18 private int mCrital; 19 //滑动辅助类 20 private Scroller mScroller; 21 //当前展示的子View的索引 22 private int showViewIndex; 23 24 public HorizontalScrollView(Context context){ 25 this(context,null); 26 } 27 28 public HorizontalScrollView(Context context, AttributeSet attributeSet){ 29 super(context,attributeSet); 30 init(context); 31 } 32 33 /** 34 * 初始化 35 * @param context 36 */ 37 public void init(Context context) { 38 //读取屏幕相关的长宽 39 wm = ((Activity)context).getWindowManager(); 40 mScreenWidth = wm.getDefaultDisplay().getWidth(); 41 mCrital=mScreenWidth/4; 42 mScroller=new Scroller(context); 43 showViewIndex=1; 44 } 45 46 /** 47 * 重新事件拦截机制 48 * 我们分析了view的事件分发,我们知道点击事件的分发顺序是 通过父布局分发,如果父布局没有拦截,即onInterceptTouchEvent返回false, 49 * 才会传递给子View。所以我们就可以利用onInterceptTouchEvent()这个方法来进行事件的拦截。来看一下代码 50 * 此处使用外部拦截法 51 * @param ev 52 * @return 53 */ 54 @Override 55 public boolean onInterceptTouchEvent(MotionEvent ev) { 56 int x=(int)ev.getX(); 57 int y=(int)ev.getY(); 58 boolean intercept=false; 59 switch (ev.getAction()){ 60 //按下事件不要拦截,否则后续事件都会给ViewGroup处理 61 case MotionEvent.ACTION_DOWN: 62 intercept=false; 63 if(!mScroller.isFinished()){ 64 mScroller.abortAnimation(); 65 intercept=true; 66 } 67 break; 68 case MotionEvent.ACTION_MOVE: 69 //如果是横向移动就进行拦截,否则不拦截 70 int deltaX=x-mLastX; 71 int deltaY=y-mLastY; 72 if(Math.abs(deltaX)>Math.abs(deltaY)){ 73 intercept=true; 74 }else { 75 intercept=false; 76 } 77 break; 78 case MotionEvent.ACTION_UP: 79 intercept=false; 80 break; 81 } 82 mLastX = x; 83 mLastY = y; 84 return intercept; 85 } 86 87 88 @Override 89 public boolean onTouchEvent(MotionEvent event) { 90 int x = (int) event.getX(); 91 int y = (int) event.getY(); 92 switch (event.getAction()) { 93 case MotionEvent.ACTION_DOWN: 94 if(!mScroller.isFinished()){ 95 mScroller.abortAnimation(); 96 } 97 break; 98 case MotionEvent.ACTION_MOVE: 99 int deltaX = x - mLastX; 100 /** 101 * scrollX是指ViewGroup的左侧边框和当前内容左侧边框之间的距离 102 */ 103 int scrollX=getScrollX(); 104 if(scrollX-deltaX>0 105 && (scrollX-deltaX)<=(mMeasureWidth-mScreenWidth)) { 106 scrollBy(-deltaX, 0); 107 } 108 break; 109 case MotionEvent.ACTION_UP: 110 scrollX=getScrollX(); 111 int dx; 112 //计算滑动的差值,如果超过1/4就滑动到下一页 113 int subScrollX=scrollX-((showViewIndex-1)*mScreenWidth); 114 if(Math.abs(subScrollX)>=mCrital){ 115 boolean next=scrollX>(showViewIndex-1)*mScreenWidth; 116 if(showViewIndex<3 && next) { 117 showViewIndex++; 118 }else { 119 showViewIndex--; 120 } 121 } 122 dx=(showViewIndex - 1) * mScreenWidth - scrollX; 123 smoothScrollByDx(dx); 124 break; 125 } 126 mLastX = x; 127 mLastY = y; 128 return true; 129 } 130 131 132 /** 133 * 缓慢滚动到指定位置 134 * @param dx 135 */ 136 private void smoothScrollByDx(int dx) { 137 //在1000毫秒内滑动dx距离,效果就是慢慢滑动 138 mScroller.startScroll(getScrollX(), 0, dx, 0, 1000); 139 invalidate(); 140 } 141 142 @Override 143 public void computeScroll() { 144 if (mScroller.computeScrollOffset()) { 145 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 146 postInvalidate(); 147 } 148 } 149 }
从上面代码中,我们看到我们只是很简单的采用横向滑动距离和垂直滑动距离进行比较来判断滑动方向。在滑动过程中,当水平方向的距离大时就判断为水平滑动,否者就是垂直滑动。