在项目里经常会有一些需求 是我们原生控件做不出的效果,所以只能我们自己去绘制,但同时自定义view,就不会自己去化解一些冲突,所以这个时候需要我们做一些代码防范。
1.滑动冲突:一下是解决滑动冲突的几点思路:
就简单举个例子 :场景如下
拿一个电商APP为例,最外一层是一个ScrollView,包裹着recycleView ,是一个商品列表,下面是一个WebView,(需求流程是当recycleView滑到底部时候才全体滑动),往往大家都会有这么一个问题,就是滑到中间或者下方,就出现互动冲突整体跟着滑动,recyclerView还没有滑动完毕,就跟着一起滑动了。
而滑动场景往往是一下三种:
1、外部滑动和内部滑动方向不一致
对于图一场景,平时工作中最常见的使用大概是外层为PageView,内层为一个Fragment+ListView/RecyclerView了。当然控件PageView和RecyclerView对事件冲突做了处理的,所以平时使用这两个控件的时候不会感受到滑动冲突的存在。如果是ScrollView+GridView等这类组合,就需要解决冲突了。
2、外部滑动和内部滑动方向一致
同样,这种场景除了图二中的内外都是上下滑动的情况外,还包括内外到时左右滑动的场景了。ScollView(垂直滚动)+ListView的组合就是比较常见的场景。
三、滑动冲突三种场景的处理思路
1、内外滑动方向不一致时处理思路
这一类场景其实比较容易分析,因为外层和内层滑动的方向不一致,所以根据手势的动向来确定把事件给谁。默认情况下,当点击内层控件时,事件会先一层层从外层传到内层,由内层来处理。这里以外层为左右滑动,内层为上下滑动为例。当判定手势的滑动为左右时,需要外层来消费事件,所以外层将事件拦截,即在外层的onInterceptTouchEvent中检测为ACTION_MOVE时返回true;而如果判定手势的滑动为上下时,需要内层来消费事件,外层不需要拦截,事件会传递到内层来处理(具体的代码实现,在后面会详细列出)。这样就通过判断滑动的方向来决定事件的处理对象,从而解决滑动冲突的问题。
那么,如何来判定手势的滑动方向呢?最常用的办法就是比较水平和竖直方向上的位移值来判断。 MotionEvent事件包含了事件的坐标,只要记录一次移动事件的起点和终点坐标,如下图所示,通过比较在水平方向的位移|dx|和|dy|的大小,来决定滑动的方向:|dy|>|dx|,本次移动的方向认为是竖直方向;反之,则认为是水平方向。当然,还可以通过夹角α的大小、斜率、速率等方式来作为判断条件。
2、内外滑动方向一致时处理思路
这种场景要比上面一种复杂一些,因为滑动方向一致,所以无法通过上述的方式来判断将事件交给谁处理。在这种情况下,往往需要根据业务的需要来判定谁来处理事件。比如竖直方向的ScrollView嵌套ListView的场景下,手指在ListView上上下滑动时:当ListView滑动到顶部且手势向下时,显然ListView不能再向下滑动了,这种情况下事件需要被外层控件拦截,由ScrollView来消费;当ListView滑动到底部且手势向上时,显然ListView也不能再向上滑动了,这种情况下事件也需要被外层控件拦截,由ScrollView来消费;其它情况下,ScrollView就不能再拦截了,滑动事件就需要由ListView来消费了,即此时上下滑动时,滑动的是ListView,而不是ScrollView。后面会以这为案例进行编码实现。
3、多层滑动嵌套时处理思路
场景3看起来比较复杂,但前面也说过了,也是由前面两种场景嵌套形成的。所以在处理场景的处理方式,就是将其拆分为简单的场景,然后按照前面的场景分析方式来处理。
四、滑动冲突的两种解决套路
1、外部拦截法
顾名思义,就是在外部滑动控件中处理拦截逻辑。这需要外部控件重写父类的onInterceptTouchEvent方法,在其中判断什么时候需要拦截事件由自身处理,什么时候需要放行将事件传给内层控件处理,内部控件不需要做任何处理。这个“套路”的伪代码表示所示:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
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;
}
return intercepted;
}
2、内部拦截法
顾名思义,就是将事件是否需要拦截的逻辑,放到内层控件中来处理。这种方式需要结合requestDisllowInterceptTouchEvent(boolean),在内层控件的重写方法dispatchTouchEvent中,根据逻辑来决定外层控件何时需要拦截事件,何时需要放行。伪代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要处理改事件) {
//允许外层控件拦截事件
getParent().requestDisallowInterceptTouchEvent(false);
} else {
//需要内部控件处理该事件,不允许上层viewGroup拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
//外层添加:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
ACTION_DOWN事件仍然不能拦截,ACTION_DOWN时会初始化一些状态和标志位等变量,requestDisllowInterceptTouchEvent(boolean)作用会失效。这里再顺便强调一下,不明白的可以去上一篇文章中阅读这部分内容。
这种方式比“外部拦截法”稍微复杂一些,所以一般推荐使用前者。同前者一样,这也是一个套路用法,无论是之前提到的何种场景,只要根据实际判断条件修改上述if语句即可。
代码示例:
1.外部拦截问题示例参考方案:
2.内部拦截问题示例参考方案: