我们在开发过程中,难免会用到ScrollView嵌套ViewPager的情况,比如淘宝商品详情页面。但当我们用普通的ScrollView嵌套ViewPager是,会出现滑动冲突的情况,原因很简单:当我们左右滑动ViewPager时,我们的手指会有一点上下滑动的浮动,而ScrollView监听了上下滑动事件,这就造成滑动冲突。
解决办法也很容易,我们只需要重写ScrollView,在ScrollView判断手指滑动的y方向的距离,如果滑动距离的y方向距离大于x方向距离,则说明用户是进行上下滑动ScrollView操作,响应ScrollView滑动事件;否则传递给子控件处理。
直接看代码:
<span style="font-size:14px;">public class ViewPagerScroller extends ScrollView {
// 滑动距离及坐标
private float xDistance, yDistance, xLast, yLast;
public ViewPagerScroller(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ViewPagerScroller(Context context) {
super(context);
}
public ViewPagerScroller(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
xLast = ev.getX();
yLast = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - xLast);
yDistance += Math.abs(curY - yLast);
xLast = curX;
yLast = curY;
if(xDistance > yDistance){
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}</span>
在重写ScrollView时,我们主要重写了onInterceptTouchEvent()方法,有些人对该方法不是很了解,下面对该方法简要说明。
onInterceptTouchEvent():用于处理事件(类似于预处理,当然也可以不处理)并改变事件的传递方向,也就是决定是否允许Touch事件继续向下(子控件)传递,一但返回True(代表事件在当前的viewGroup中会被处理),则向下传递之路被截断(所有子控件将没有机会参与Touch事件),同时把事件传递给当前的控件的onTouchEvent()处理;返回false,则把事件交给子控件的onInterceptTouchEvent()
了解了如何解决冲突后,我们要深入一下为什么这样就解决冲突了,ScrollView源码有没有做过响应的处理。我们大致看一下ScrollView监听事件得源码:
<span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) { // <<=================注意看这里
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return mIsBeingDragged;
}</span>
由于ScrollView是ViewPager的父容器,由他来决定事件是自己处理还是传递给ViewPager来处理,当onInterceptTouchEvent()返回true是,说明时间有自己拦截并处理,当返回false说明事件传递给子容器(ViewPager)来处理。
我们看一下源码标记的地方,ScrollView在监听ACTION_MOVE事件时,为y方向滑动距离做了一次比较,如果滑动的距离大于一个值(mTouchSlop),则说明是垂直滑动,返回true而响应事件,如果不大于,就交给子View响应。看完源码后可能会有这样疑问,ScrollView已经做了判断,也就是说做了滑动冲突的处理了,那么问题来了,滑动ViewPager时可为什么还会有冲突的存在呢?
其实细心的朋友会注意到y是和mTouchSlop进行大小比较的,二我们重写是使用y和x的活动距离进行比较的。说道这里我们很容易就明白了,原来都是mTouchSlop的过,可他是什么值呢?我们在源码中可以看到:mTouchSlop = configuration.getScaledTouchSlop();
原来他是系统的一个常量,可他是多少呢?其实他就是系统所能识别出来的被认为是滑动的最小距离,他是一个常量,不同的设备,他的值可能不同。
很多人看到这里就明白了为什么源码不能解决我们的冲突了,源码用Y方向滑动的距离与mTouchSlop 作比较,只要达到系统认为能够滑动的最小距离,就拦截该事件,所以我们滑动ViewPager时,如果手指平行滑动,就可以滑动ViewPager,可如果稍微有点倾斜,就会差生冲突,因为此时,时而ViewPager处理事件,时而ScrollView处理事件,所以我们就会看到两个View都在微微的滑动。
我们解决冲突时就是让横向和纵向来比较大小,这样比较区分度很明显,ScrollView也就知道什么时候事件该他处理,什么时候事件该交给子VIew处理。
这里只是分享了冲突的一个例子,后面会详细讲解View的时间分发机制,以便于深入理解事件的分发和冲突的解决!!