一、常见的滑动冲突场景
场景1——外部滑动方向和内部滑动方向不一致,如:ViewPager中有多个fragment,而fragment中有ListView,这时ViewPager可以左右滑动,而ListView可以上下滑动,这就造成了滑动冲突。注意:这只是举个例子说明一下场景1,事实上ViewPager内部已经处理了这种滑动冲突,在采用ViewPager时,我们无需关注这个问题。
场景2——外部滑动方向和内部滑动方向一致。
场景3——上述两种场景的嵌套,即共有三层,外层与中层的滑动方向一致,而中层与内层的滑动方向不一致。
二、滑动冲突的处理规则
场景1的处理规则:
1、当用户左右滑动时,让外部的View拦截点击事件,当用户上下滑动时,让内部的View拦截点击事件;
2、判断用户的滑动方向(左右、上下):如果用户手指滑动的水平距离大于垂直距离,则左右滑动,反之,上下滑动;还可以根据角度、速度差来做判断;
场景2的处理规则:
无法根据滑动的角度、距离差、速度差来判断,因为场景2内部、外部的滑动方向一致;这时候一般都能在业务上找到突破点,如业务上规定:当处于某种状态需要外部View响应用户的滑动,而处于另一种状态时则需要内部View响应用户的滑动,所以我们可以根据业务的需求得出相应的处理规则。
场景3的处理规则:场景1的处理规则和场景2的处理规则一起用。
三、滑动冲突的解决方法
以下两种解决方法都是通用的方法:
1、外部拦截法
我们都知道点击事件是先经过父容器的拦截处理的,所以我们就可以在父容器的拦截方法中写下自己的逻辑代码即可,相当于我们要重写一个布局Layout,使其继承ViewGroup或其它的布局(LinearLayout等),然后重写其onInterceptTouchEvent方法。外部拦截法的伪代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int)ev.getX();
int y = (int)ev.getY();
switch (ev.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;
}
解释上述代码:
1、父容器不拦截ACTION_DOWN事件:那是因为一旦父容器拦截了ACTION_DOWN事件,后续的ACTION_MOVE、ACTION_UP事件都会直接交给父容器处理,这个时候事件无法传递给子元素了。
2、父容器不拦截ACTION_UP事件:首先我们要知道onClick 事件是在ACTION_UP事件之后执行的,那当子元素有一个onClick事件,而这时候父容器拦截了ACTION_UP事件,那子元素的onClick事件就无法执行了。
3、ACTION_MOVE事件:在这里可以根据我们的需求,来判断父容器是否需要拦截事件,需要则返回true,否则返回false。
总结,外部拦截法比较简单,实现起来较容易。
2、内部拦截法
首先我们了解下ViewGroup.requestDisallowInterceptTouchEvent(boolean)方法,此方法就是在子View中通知父容器拦截或不拦截点击事件,false ——拦截,true ——不拦截;
内部拦截法的思想是父容器先不拦截任何事件,即所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这时我们就用到requestDisallowInterceptTouchEvent 方法了。
具体做法:我们重写子View的dispatchTouchEvent方法,即我们新建一个类继承子View,然后重写它的dispatchTouchEvent方法,伪代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//获得当前的位置坐标
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//通知父容器不要拦截事件
horizontalScrollLayout.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要此事件){
//通知父容器拦截此事件
horizontalScrollLayout.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
//重置手指的初始位置
mLastY = y;
mLastX = x;
return super.dispatchTouchEvent(ev);
}
解释上述代码:
1、horizontalScrollLayout是此子View的父容器的对象,我们可以在子 View中定义一个方法:
public void setHorizontalScrollLayout(HorizontalScrollLayout horizontalScrollLayout) {
this.horizontalScrollLayout = horizontalScrollLayout;
}
从而得到父容器的对象,然后就可以利用该对象通知父容器拦截或不拦截事件了。
2、我们还要重写父容器中的onInterceptTouchEvent方法,如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
此时,父容器拦截了除ACTION_DOWN以外的其它事件,那为什么这样做?
原因:只有这样,才能使的当子元素调用了requestDisallowInterceptTouchEvent(false)方法后,父容器才能继续拦截所需的事件。
3、那父容器为什么不能拦截ACTION_DOWN事件呢?
原因:因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截了ACTION_DOWN事件,那么所有的事件都无法传递给子元素了,这样内部拦截就起不到作用了。
总结,内部拦截法比较复杂,在实际应用中,建议还是用外部拦截法。
四、举例说明
在举例的时候,我自定义了几个view,大家主要看其中的dispatchTouchEvent方法和onInterceptTouchEvent方法,至于自定View的方法会在之后的博客中详细描述。
场景1的解决方案:外部拦截和内部拦截的方法都有 。代码下载请点我