ViewPager的滑动手势冲突解决方案
所有解决滑动冲突的方法都避免不了要从
DispatchTouchEvent(MotionEvent ev); 事件分发
OnInterceptTouchEvent(MotionEvent ev); 事件拦截
OnTouchEvent(Motion ev); 事件消费
这三个回调方法入手,搞明白这个三个方法之间含义和关系,Android系统中的事件传递就基本明白了
1、OnInterceptTouchEvent(MotionEvent ev);
ViewPager里面没有复写DispatchTouchEvent方法,所以我们先从OnInterceptTouchEvent方法入手。
OnInterceptTouchEvent方法决定了ViewPager是否拦截当前的手势。如果返回true,则会调用OnTouchEvent方法,并在其中执行真正的滚动操作。
1.1 首先获取当前的手势动作,包括ACTION_DOWN、ACTION_MOVE、ACTION_CANCEL、ACTION_UP等。
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
注:上述获得Action的方法中有一个按位与操作,目的是取得执行过的动作的掩码值,不包括点的索引值。
1.2 处理ACTION_CANCEL和ACTION_UP事件
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the drag.
if (DEBUG) Log.v(TAG, "Intercept done!");
resetTouch();
return false;
}
首先判断当前手势是否是取消或抬起动作,如果是的话,则执行resetTouch()方法,并返回false。下面看下resetTouch()做了什么:
private boolean resetTouch() {
boolean needsInvalidate;
mActivePointerId = INVALID_POINTER;
endDrag();
needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
return needsInvalidate;
}
private void endDrag() {
mIsBeingDragged = false;
mIsUnableToDrag = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
很简单,resetTouch()方法主要是做了一些变量重置、边界效果释放的操作。
加一个疑问:如果ViewPager处于横向滑动的中间,此时抬起手指,ViewPager会自动切换到上一页或下一页。这个操作并没有添加到resetTouch()方法。
1.3 处理非ACTION_DOWN事件
//如果不是ACTION_DOWN手势,检查当前拖动状态
if (action != MotionEvent.ACTION_DOWN) {
//如果正在被拖动,则返回true。Touch事件由ViewPager处理
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
//如果不能被拖动,返回false。Touch由子视图处理
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
1.4 处理ACTION_DOWN事件
case MotionEvent.ACTION_DOWN: {
//获得点击位置
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
//获取指针id,并标记为活动态。这个id在整个触摸事件过程中是不变的
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
//标记为可拖拽
mIsUnableToDrag = false;
//标记开始滚动
mIsScrollStarted = true;
//通知Scroller计算需要滚动的偏移
mScroller.computeScrollOffset();
//SCROLL_STATE_SETTLING是一种趋向于最终位置过程的状态,也就是ViewPager正在向最终位置位移。
//如果ViewPager正在向最终位置滚动,并且需要滚动的距离大于最小距离,就终止滚动动画。也就是说允许用户控制住ViewPager,尽管ViewPager处于动画的状态
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
// Let the user 'catch' the pager as it animates.
//通知Scroller终止动画
mScroller.abortAnimation();
mPopulatePending = false;
//populate()是一个关键方法,下面会说到
populate();
//设置正在由用户拖拽标志位
mIsBeingDragged = true;
//设置父视图不要拦截触摸事件,由ViewPager处理
requestParentDisallowInterceptTouchEvent(true);
//设置滚动状态为“正在拖拽” setScrollState(SCROLL_STATE_DRAGGING);
} else {
//正常状态下应该走此逻辑。
//
completeScroll(false);
mIsBeingDragged = false;
}
break;
}
1.5 处理ACTION_MOVE手势
case MotionEvent.ACTION_MOVE: {
//基本动作就是检查用户手指是否移动了足够的距离,然后决定是否滚动ViewPager
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
//如果处于活动的触摸指针id是无效的,表明没有按压到内容上,直接返回。
break;
}
//根据手指id,获取手指在多指触控列表中的索引
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
//取到当前手指位移后的坐标,和ACTION_DOWN手势时获取原始坐标做对比,计算出x和y方向的差值的绝对值。
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
//接下来就是判断是否可拖拽
//dx!=0 横向滑动
//mGutterSize ViewPager左右两边预留一定宽度的滑动无效区域,这个区域宽度是16dp和ViewPager宽度十分之一两者的最小值。
//!isGutterDrag(mLastMotionX, dx) 触摸点不在滑动无效区域内
//canScroll() 检查子视图横向可滚动性
//如果上述条件都满足,表明由嵌套的视图处理当前触摸事件。同时设置mIsUnableToDrag标志位为true
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;
}
//xDiff > mTouchSlop 横向拖拽手势
//xDiff * 0.5f > yDiff 横向移动距离的一半大于纵向移动距离
//符合以上条件,表明用户想要横向滑动ViewPager,由ViewPager处理触摸事件
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
mIsBeingDragged = true;
//禁止父视图处理事件 requestParentDisallowInterceptTouchEvent(true);
//设置状态为正在拖拽 、setScrollState(SCROLL_STATE_DRAGGING);
//修正原始触摸点坐标
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
//这只子视图不缓存绘制数据
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
//用户纵向滑动,不能拖拽ViewPager
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
//performDrag(float x) 方法是处理拖拽的关键方法,该方法的返回值代表了是否需要进行由边界动画引起的页面刷新操作。
if (performDrag(x)) {
//如果拖拽到边界,会引起边界动画EdgeEffect.onPull(float deltaDistance),动画结束后一定要调用宿主View的invalidate()方法 ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
1.6 为速度追踪器VelocityTracker添加手势事件
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
//经过上述几步,最终能判断出是否能够拖拽。只有在能够拖拽的情况下才拦截触摸手势
return mIsBeingDragged;
总结:ViewPager属于只可以横向滚动的布局。通过分析其OnInterceptTouchEvent方法可以看出,横向拖拽手势肯定由ViewPager处理,通过设置requestParentDisallowInterceptTouchEvent(true)方法,禁止父视图处理触摸事件实现。纵向拖拽手势由子视图处理,OnInterceptTouchEvent方法直接返回false
最后,上面留下一个疑问:手指抬起时,ViewPager自动滚到上一页或下一页的操作没有放到resetTouch()方法中,而是在OnTouchEvent(MotionEvent ev)回调中对ACTION_UP和ACTION_CANCEL的判断分支中。
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int width = getClientWidth();
final int scrollX = getScrollX();
final ItemInfo ii = infoForCurrentScrollPosition();
final int currentPage = ii.position;
final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
final int activePointerIndex =
MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final int totalDelta = (int) (x - mInitialMotionX);
//根据当前页号、页面偏移、滑动速度、手指滑动总长度,计算出下一页的页号
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
//切换卡片
setCurrentItemInternal(nextPage, true, true, initialVelocity);
needsInvalidate = resetTouch();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
//如果是取消事件,则滚回到原始页号。scrollToItem方法是核心滚动方法
scrollToItem(mCurItem, true, 0, false);
needsInvalidate = resetTouch();
}
break;
ViewPager滚动动作分析
private void scrollToItem(int item, boolean smoothScroll, int velocity,
boolean dispatchSelected) {
//获取到要滚到的节点信息
final ItemInfo curInfo = infoForPosition(item);
int destX = 0;
if (curInfo != null) {
final int width = getClientWidth();
//计算滚动距离
destX = (int) (width * Math.max(mFirstOffset,
Math.min(curInfo.offset, mLastOffset)));
}
//判断是否是平滑的滚动
//smoothScrollTo方法是由Scroller执行滚动
//ScrollTo方法是由View类的onScrollChanged方法执行滚动
if (smoothScroll) {
smoothScrollTo(destX, 0, velocity);
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
completeScroll(false);
scrollTo(destX, 0);
pageScrolled(destX);
}
}
ViewPager拖拽动作分析
private boolean performDrag(float x) {
boolean needsInvalidate = false;
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
float oldScrollX = getScrollX();
float scrollX = oldScrollX + deltaX;
final int width = getClientWidth();
float leftBound = width * mFirstOffset;
float rightBound = width * mLastOffset;
boolean leftAbsolute = true;
boolean rightAbsolute = true;
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
leftAbsolute = false;
leftBound = firstItem.offset * width;
}
if (lastItem.position != mAdapter.getCount() - 1) {
rightAbsolute = false;
rightBound = lastItem.offset * width;
}
if (scrollX < leftBound) {
if (leftAbsolute) {
float over = leftBound - scrollX;
needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
}
scrollX = leftBound;
} else if (scrollX > rightBound) {
if (rightAbsolute) {
float over = scrollX - rightBound;
needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
}
scrollX = rightBound;
}
// Don't lose the rounded component
mLastMotionX += scrollX - (int) scrollX;
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
return needsInvalidate;
}