1.Android滑动事件冲突解决办法
滑动事件的冲突包括两种情形:
- 不同方向的滑动冲突:比如ScrollView内部嵌套banner显示的viewPager
- 同一方向事件滑动冲突:比如ScrollView内部嵌套ListView,ListView下拉刷新功能,需要ListView自身滑动实现滑动,但是当滑动到头部时需要ListView和Header一起滑动,也就是整个父容器的滑动,这时会产生滑动冲突.
2.不同方向滑动冲突的解决方案
一般根据在横坐标上滑动的距离与纵坐标滑动的距离相比较,如果横坐标上view滑动的距离大于纵坐标上view滑动的距离,则父view不进行事件拦截,否则父view进行事件拦截自己消费滑动事件,实现方式如下:
2.1外部拦截法:重写父view的拦截事件,由父view决定是否要拦截事件:
@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 {
resume = true;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return resume;
}
2.2 内部拦截法:在view的onTouch方法中每次进入就设定父View不拦截此次事件,然后在MOTION_MOVE时候,根据滑动的距离判断再决定是父View是否有权利拦截Touch事件(即滑动行为)。
parentView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
view.getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
});
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
view.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)) {
view.getParent().requestDisallowInterceptTouchEvent(false);
} else {
view.getParent().requestDisallowInterceptTouchEvent(true);
}
default:
break;
}
return false;
}
});
3.相同方向滑动冲突的解决方案
对于这种情况,比较特殊,我们没有通用的规则,得根据业务逻辑来得出相应的处理规则。举个最常见的例子,ListView下拉刷新功能,需要ListView自身滑动实现滑动,但是当滑动到头部时需要ListView和Header一起滑动,也就是整个父容器的滑动,这就涉及到滑动冲突问题了,如果不处理好滑动冲突,就会出现各种意想不到情况。对于这种情况的解决,我们可以采用拦截法:
3.1.外部拦截法(由父容器决定事件的传递):让事件都经过父容器的拦截处理(onInterceptTouchEvent ),如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法,其伪代码如下:
1.首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器(onTouchEvent),子元素就没有机会处理事件了。
2.其次是up事件也返回了false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。
3.2内部拦截法(自己决定事件的传递):父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法(请求父类不要拦截,返回值为true时不拦截,返回值为false时为拦截)交给父容器处理,称为内部拦截法,使用起来稍显麻烦,伪代码如下:
首先我们需要重写子元素的dispatchTouchEvent方法:
然后修改父容器的onInterceptTouchEvent方法:
//同一方向上的外部拦截法,ScrollView包括listView
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev!=null && containerArea(ev)){//如果子view listView在区域内说明子View需要消费该事件,
// 父View ScrolloView不拦截事件,交给子View listView去执行
return false;
}
return true;
}
public void setListView(ListView listView){
this.listView=listView;
}
//判断是否在区域内
private boolean containerArea(MotionEvent event){
float eventX = event.getRawX();
float eventY= event.getRawY();
int [] outLocation =new int[2];
listView.getLocationOnScreen(outLocation);
float l = outLocation[0];
float t = outLocation[1];
float r = l+listView.getWidth();
float b = t +listView.getHeight();
if(eventX>=l&&eventX<=r&&eventY>=t&&eventY<=b){
return true;
}
return false;
}