叙述
滑动冲突可以说是日常开发中比较常见的一类问题,也是比较让人头疼的一类问题,尤其是在使用第三方框架的时候,两个原本完美的控件,组合在一起之后,忽然发现整个世界都不好了。
关于滑动冲突
滑动冲突分类
滑动冲突,总的来说就是两类。
同方向滑动冲突
比如ScrollView嵌套ListView,或者是ScrollView嵌套自己不同方向滑动冲突
比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,这种情况其实很典型。现在大部分应用最外层都是ViewPager+Fragment 的底部切换(比如微信)结构,这种时候,就很容易出现滑动冲突。不过ViewPager里面无论是嵌套ListView还是ScrollView,滑动冲突是没有的,毕竟是官方的东西,可能已经考虑到了这些,所以比较完善。
复杂一点的滑动冲突,基本上就是这两个冲突结合的结果。
滑动冲突解决思路
滑动冲突,就其本质来说,两个不同方向(或者是同方向)的View,其中有一个是占主导地位的,每次总是抢着去处理外界的滑动行为,这样就导致一种很别扭的用户体验,明明只是横向的滑动了一下,纵向的列表却在垂直方向发生了动作。就是说,这个占主导地位的View,每一次都身不由己的拦截了这个滑动的动作,因此,要解决滑动冲突,就是得明确告诉这个占主导地位的View,什么时候你该拦截,什么时候你不应该拦截,应该由下一层的View去处理这个滑动动作。
这里不明白的同学,可以去了解一下Android Touch事件的分发机制,这也是解决滑动冲突的核心知识。
第二种滑动冲突,解决起来是比较简单的。这里就结合例子说一下。
滑动冲突
这里,说一下背景情况。之前做下拉刷新、上拉加载更多时一直使用的是PullToRefreshView这个控件,因为很方便,不用导入三方工程。在其内部可以放置ListView,GridView及ScrollView,非常方便,用起来可谓是屡试不爽。但是直到有一天,因项目需要,在ListView顶部加了一个轮播图控件BannerView(这个可以参考之前写的一篇学习笔记)。结果发现轮播图滑动的时候,和纵向的下拉刷新组件冲突了。
如之前所说,解决滑动冲突的关键,就是明确告知接收到Touch的View,是否需要拦截此次事件。
解决方法
解决方案1,从外部拦截机制考虑
这里,相当于是PullToRefreshView嵌套了ViewPager,那么每次优先接收到Touch事件的必然是PullToRefreshView。这样就清楚了,看代码:
在PullToRefreshView中:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int y = (int) e.getRawY();
int x = (int) e.getRawX();
boolean resume = false;
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// 首先拦截down事件,记录y坐标
mLastMotionY = y;
mLastMotionX = x;
resume = false;
break;
case MotionEvent.ACTION_MOVE:
// deltaY > 0 是向下运动,< 0是向上运动
int deltaY = y - mLastMotionY;
int deleaX = x - mLastMotionX;
if (Math.abs(deleaX) > Math.abs(deltaY)) {
resume = false;
} else {
//当前正处于滑动
if (isRefreshViewScroll(deltaY)) {
resume = true;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return resume;
}
这里最关键的代码就是这行
if (Math.abs(deleaX) > Math.abs(deltaY)) {
resume = false;
}
横向滑动距离大于纵向时,无须拦截这次滑动事件。其实,就是这么简单,但前提是你必须明确了解Android Touch事件的传递机制,期间各个方法执行的顺序及意义。
解决方案2,从内容逆向思维分析
有时候,我们不想去修改引入的第三方控件,或者说是无法修改时。就必须考虑从当前从Touch传递事件中最后的那个View逆向考虑。首先,由Android中View的Touch事件传递机制,我们知道Touch事件,首先必然由最外层View拦截,如果无法更改这个最外层View,那么是不是就没辙了呢?其实不然,Android这么高大上的系统必然考虑到了这个问题,好了废话不说,先看代码
private BannerView carouselView;
private Context mContext;
private PullToRefreshView refreshView;
.........
refreshView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
carouselView.getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
});
carouselView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
carouselView.getParent().requestDisallowInterceptTouchEvent(true);
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int deltaY = y - lastY;
int deltaX = x - lastX;
if (Math.abs(deltaX) < Math.abs(deltaY)) {
carouselView.getParent().requestDisallowInterceptTouchEvent(false);
} else {
carouselView.getParent().requestDisallowInterceptTouchEvent(true);
}
default:
break;
}
return false;
}
});
首先说一下这个方法
public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)
Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.
Parameters
disallowIntercept
True if the child does not want the parent to intercept touch events.
API里的意思很明确,子View如果不希望其父View拦截Touch事件时,可调用此方法。当disallowIntercept这个参数为true时,父View将不拦截。
PS:这个方法的命名和其参数的使用逻辑,让我想到了一句很有意思的话,敌人的敌人就是朋友,真不知道Google的大神们怎么想的,非要搞一个反逻辑。
好了,言归正传。这里拦截直接也很明确,在carouselView的onTouch方法中每次进入就设定父View不拦截此次事件,然后在MOTION_MOVE时候,根据滑动的距离判断再决定是父View是否有权利拦截Touch事件(即滑动行为)。
好了,这里可以看到,解决这种滑动冲突的方法很简单,最根本的还是得充分了解Touch事件的传递机制,只有这样,才能明白该在哪里做什么事情。
当然,横竖滑动的冲突很好理解,但同一方向的滑动冲突情况就有点复杂了,下次再说。