起因
主页的某一个Tab有是一个类似与新闻客户端的多tab页面,如在用户要滑动的Banner活动图的时候,时而会触发切换tab,时而会触发切换banner导致操作上的一种不适感
现象大致分析
- 即使不看源码,凭感觉也可以知道肯定是ViewPager的在处理Move行为的时候没有判断好(因为down的时候肯定是不知道让哪一个组件来处理的)
需要达到的效果
- 在Banner处滑动的时候,交由Banner来处理
- 在Banner下方列表处上下滑动交由该tab下的recycleview来处理(也就是不需要任何改动)
- 在Banner下方列表处左右滑动交由ViewPager来处理
源码分析
我们直接来看ViewPager的onInterceptTouchEvent方法,毕竟是这个方法来处理事件的拦截与否
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;
}
case MotionEvent.ACTION_DOWN: {
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = ev.getPointerId(0);
mIsUnableToDrag = false;
mIsScrollStarted = true;
mScroller.computeScrollOffset();
if (mScrollState == SCROLL_STATE_SETTLING
&& Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
// Let the user 'catch' the pager as it animates.
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
} else {
completeScroll(false);
mIsBeingDragged = false;
}
if (DEBUG) {
Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ " mIsBeingDragged=" + mIsBeingDragged
+ "mIsUnableToDrag=" + mIsUnableToDrag);
}
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
返回值为mIsBeingDragged,并且上文说了只需要看Move的行为,那么可以得知我们只需要看ViewPager什么时候返回了false(也就是你不要拦我啊,我要这个行为给我的孩子)
仅需要看这一段
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;
}
- dx!=0不需要关注,必然符合
- isGutterDrag表示是否是在ViewPager的缝隙处滑动,不需要关注
- 重点来了,就是这个canScroll方法
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
&& y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
&& canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
紧接着
- 可以看到,这个方法的目的就是在于遍历子View,孙View等里是否有可以横向滚动的组件并且落点在组件内部的
- 关键方法ViewCompat.canScrollHorizontally,按理来说RecycleView设置了横向的LinearLayoutManager,应该是没问题才对
- 进到RecycleView源码可知,并没有重写canScrollHorizontally,而是在合适的时候(比如onTouch)返回了mLayout.canScrollHorizontally()
处理方案
-
方案一
重写RecycleView的canScrollHorizontally,直接返回为true
经测试,有效
-
方案二
手动处置ViewPager的onInterceptTouchEvent,添加监听,在监听中判断落点是否在Banner的组件里,并且在RecycleView重写dispatchTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mMoveListener == null ? !noScroller && super.onInterceptTouchEvent(ev) : mMoveListener.onMoveTouchEvent(ev) ? super.onInterceptTouchEvent(ev) : false;
}
private OnMoveTouchListener mMoveListener;
public void setOnMoveListener(OnMoveTouchListener l) {
mMoveListener = l;
}
public interface OnMoveTouchListener {
boolean onMoveTouchEvent(MotionEvent ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getY() > getMeasuredHeight())
return false;
return super.dispatchTouchEvent(ev);
}
- 还没完,还需要判断当前界面是否显示
@Override
public boolean onMoveTouchEvent(MotionEvent ev) {
boolean b = getFragmentVisible() ? !mModulePage.getBannerPager().dispatchTouchEvent(ev) : false;
return b;
}
总之非常的麻烦,效果也能达到,还是方案一好,完结