从事android开发的时候,经常会自定义控件,这时候会碰到一个问题,就是横向和竖向的滑动冲突。试想如果你在横向滑动一个View,然后不小心突然竖向的控件滑动了一下,这时候横向滑动的控件就无法接收到滑动事件了,造成了特别不好的体验。比如这样:
其实解决方法已经在源码中有提现,ViewPager作为一个横向的ViewGroup就已经解决了冲突,给我们写自定义控件时提供解决思路。在看源码之前需要先了解下事件分发的顺序。
事件分发简单流程
事件分发要将清楚需要重新开博客来写才行,不是一两句话就可以说明白。这里就简单的来理一下大概顺序。触摸事件首先会进入ViewGroup的dispatchTouchEvent(),我们来简单看一下。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//这里通过判断disallowIntercept来决定是否调用onInterceptTouchEvent()
//默认是false,这个变量是内部拦截的重点
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//...
if (!canceled && !intercepted) {
//...这里做事件分发到子控件
}
}
注意看我中文注释的地方,这里我们知道会先调用onInterceptTouchEvent(),通过返回值来判断是否拦截事件或者分发下去。如果分发事件就会到了View的dispatchTouchEvent(),然后会判断调用OnTouchEvent(),中间还有很多判断,但我们暂定这样的流程,因为不是该博文的重点。
所以大致流程这样:
ViewGroup —>dispatchTouchEvent —->onInterceptTouchEvent—->
View —->dispatchTouchEvent —–>onTouchEvent
ViewPager源码分析
我们把情况设想的简单一些,便于分析。假设一个ScrollView中间套了一个ViewPager。那么按照简单的事件分发逻辑,事件经过ScrollView的onInterceptTouchEvent然后再到ViewPager的dispatchTouchEvent再到onInterceptTouchEvent。手指横向滑动ScrollView是不会拦截事件的,这里就不解释了,有兴趣的可以仔细研究研究源码。我们直接看ViewPager的onInterceptTouchEvent做了什么吧。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//...
// Nothing more to do here if we have decided whether or not we
// are dragging.
if (action != MotionEvent.ACTION_DOWN) {
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
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);
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = ev.getY(pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
&& canScroll(this, false, (int) dx, (int) x, (int) y)) {
// Nested view has scrollable area under this point. Let it be handled there.
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionX = dx > 0
? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
// The finger has moved enough in the vertical
// direction to be counted as a drag... abort
// any attempt to drag horizontally, to work correctly
// with children that have scrolling containers.
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
//...
}
//...
return mIsBeingDragged;
}
代码太长,我们还是只关注重点,在ACTION_MOVE的时候获取这次滑动的xDiff和yDiff,也就是横向和竖向滑动的距离,通过 判断 (xDiff > mTouchSlop && xDiff * 0.5f > yDiff),以及mTouchSlop 来置位mIsBeingDragged =true,从这个变量命名我们就可以知道,用来置为Viewpager已经处于滑动状态了。这个mTouchSlop 可以理解为最小滑动距离,获取方式也贴一下
final ViewConfiguration configuration = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mTouchSlop = configuration.getScaledPagingTouchSlop();
接下来 requestParentDisallowInterceptTouchEvent(true)。
private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
我就可以认为Viewpager的所有上级都已经disallowIntercept 为true了,也就是不会拦截事件了。总结一下流程,onInterceptTouchEvent通过判断滑动距离,x方向的0.5倍大于y方向的偏移距离,以及x方法的距离大于mTouchSlop 我们就可以认定Viewpager处于滑动状态。mIsBeingDragged=true,调用requestParentDisallowInterceptTouchEvent使父控件不拦截事件,并且触摸事件将不会分发到子控件。那么接下来就到了Viewpager的OnTouchEvent,我们接着看一下
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
// A child has consumed some touch events and put us into an inconsistent
// state.
needsInvalidate = resetTouch();
break;
}
final float x = ev.getX(pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = ev.getY(pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG) {
Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
}
if (xDiff > mTouchSlop && xDiff > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(activePointerIndex);
needsInvalidate |= performDrag(x);
}
break;
我们也只需要关注MOVE事件就好了,其实如果触摸事件走到这里有两种情况,一种是onInterceptTouchEvent拦截了,还有一种就是没有子控件接收事件,无论那种情况,如果mIsBeingDragged=false,这里会再次进行判断,将xDiff 的范围缩小了,也就是更容易使Viewpager处于滑动状态。也就是说,在Viewpager没有子控件接受事件的情况下,滑动状态的变更更加容易,有子控件的接受事件的情况,判定将范围将扩大,也就是xDiff需要比yDiff大两倍才会判定触发滑动。注意如果已经判定为滑动状态,后面将不会再判定,直接将处理后续的所有事件。
总结
看到这里其实思路已经比较明确了,内部拦截法就是通过横向和竖向滑动的距离的差值来判断是否拦截,然后调用 parent.requestDisallowInterceptTouchEvent(true)来通知父控件不拦截事件,这个会将所有的上层控件一一通知。Viewgroup可以参考Viewpager的写法,如果只是单个View就直接参考OnTouchEvent就可以了。