滑动冲突指的是:当父容器和子 View都可以响应滑动手势时,系统无法判断应该让哪一个控件处理滑动事件,导致滑动行为出现异常。
假设场景是这样的:
ScrollView
。HorizontalScrollView
。当用户用手指在子 View 上滑动时,用户很难做到完全垂直或水平,多数时候是斜着的。此时因为既包含水平,又包含垂直,导致父容器和子View都可以响应该滑动事件。
那我们该怎么处理呢?他有以下两种处理方法。
父容器根据需要在onInterceptTouchEvent方法中对触摸事件进行选择性拦截,如果父容器返回 true
,那么这个事件就会被父容器处理,子 View 不再接收到该事件。思路可以看以下伪代码
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 不拦截 ACTION_DOWN,交给子 View 处理
return false;
case MotionEvent.ACTION_MOVE:
int deltaX = (int) (event.getX() - mLastXIntercept);
int deltaY = (int) (event.getY() - mLastYIntercept);
// 如果是垂直滑动,父容器可以拦截事件
if (Math.abs(deltaY) > Math.abs(deltaX)) {
return true; // 父容器拦截事件
} else {
return false; // 水平滑动,交给子 View 处理
}
case MotionEvent.ACTION_UP:
// 不拦截 ACTION_UP,交给子 View 处理
return false;
default:
return super.onInterceptTouchEvent(event);
}
}
思路如下所示
ACTION_MOVE
事件**中:根据移动的x和y举例判断,如果移动的y距离大于x,那么说明用户倾向于进行垂直滑动,父容器就可以拦截事件。内部拦截法其核心思想是让 父容器不主动拦截事件,而是通过一个标记来判断是否拦截,这个标记是!disallowIntercept
,如果为真,启用拦截。
那么首先,它这个为false,不拦截,把所有事件先传递给子 View。子 View 来决定是否要自己消费事件或者交给父容器处理。关键方法是getParent().requestDisallowInterceptTouchEvent(false);
这个方法会使得父View中上述的条件为true,启用父View的拦截,子View就接收不到后续的事件列了。
思路可以看以下伪代码:
dispatchTouchEvent
方法@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 禁止父容器拦截当前事件序列,确保子 View 获取完整的事件流
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX; // 水平滑动距离
int deltaY = y - mLastY; // 垂直滑动距离
// 根据滑动方向决定事件处理权
if (Math.abs(deltaY) > Math.abs(deltaX)) {
// 如果是垂直滑动,父容器需要处理事件,允许父容器拦截当前事件列
getParent().requestDisallowInterceptTouchEvent(false);
} else {
// 如果是水平滑动,子 View 自己处理事件
// 注意:无需特殊处理,保持父容器不拦截即可
}
break;
}
case MotionEvent.ACTION_UP: {
// 这里通常不需要特殊处理
break;
}
default:
break;
}
// 更新上一次的触摸坐标
mLastX = x;
mLastY = y;
// 子 View 自己处理事件或继续传递
return super.dispatchTouchEvent(event);
}
onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 不拦截 ACTION_DOWN,必须交给子 View
return false;
case MotionEvent.ACTION_MOVE:
// 如果子 View 不再禁止拦截,父容器可以尝试拦截事件
//如果子 View 已经调用了 requestDisallowInterceptTouchEvent(false),则父容器会有机会拦截 ACTION_MOVE 事件。
return true;
default:
return super.onInterceptTouchEvent(event);
}
}
思路所示
ACTION_DOWN
事件,否则整个事件序列无法传递到子 View。后续事件(如 ACTION_MOVE
)的拦截权由子 View 通过 requestDisallowInterceptTouchEvent
动态 控制。dispatchTouchEvent
方法的 ACTION_MOVE 事件中,子 View 接收到所有事件,并基于滑动方向、业务逻辑等条件判断是自己处理事件还是交给父容器。如果需要交给父容器,则调用 parent.requestDisallowInterceptTouchEvent(false)
。该方法会使得父容器拦截代码判断条件 !disallowIntercept
为真,启用拦截。场景解释:
代码如下所示
滑动冲突:由于 ScrollView
是一个可以上下滚动的容器,而 ViewPager
中的内容通常是可以左右滑动的,这就导致了滑动冲突。ScrollView
和 ViewPager
都希望处理触摸事件,因此会发生冲突,造成滑动不流畅或滑动行为异常。
滑动方向不同之以ScrollView与ViewPager为例的外部解决法
举例子:以ScrollView与ViewPager为例
public class MyScrollView extends ScrollView {
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private float mDownPosX = 0;
private float mDownPosY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final float deltaY = Math.abs(y - mDownPosY);
if (deltaX > deltaY) {
return false;// 倾向于左右滑动,所以不拦截
} else {
return true;//竖直滑动,进行拦截
}
}
return super.onInterceptTouchEvent(ev);
}
}
从子View着手,父View 先不要拦截任何事件,所有的 事件传递给子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View 处理。
实现思路 如下,重写 子View 的dispatchTouchEvent方法,在Action_down动作中通过方法requestDisallowInterceptTouchEvent(true) 先请求 父View 不要拦截事件,这样保证子View能够接受到Action_move事件,再在Action_move动作中根据自己的逻辑是否要拦截事件,不要的话再交给 父View 处理
public class MyViewPager extends ViewPager {
private static final String TAG = "yc";
int lastX = -1;
int lastY = -1;
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// 保证子View能够接收到Action_move事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
if (dealtX >= dealtY) {
//左右滑,禁止父容器拦截
getParent().requestDisallowInterceptTouchEvent(true);
} else {
//上下滑,让父容器拦截后续事件列
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
}
csdn:http://my.csdn.net/qq_35829566
掘金:https://juejin.im/user/499639464759898
github:https://github.com/jjjjjjava
简书:http://www.jianshu.com/u/92a2412be53e
邮箱:[[email protected]]