一个ViewGroup的滑动肯定和它的dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()有关。ViewPager重写了后两者,我们一个个来看。首先说明一下,ViewPager根据手势产生视图移动的方式有两种,一种是MOVE的时候随手指的拖动,一种是UP之后滑动到指定页面,而滑动是通过Scroller + computeScroll()实现的。
Viewpager有一个mScrollState成员维护着ViewPager当前页面的状态,它可能被赋三个值。
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
*/
//当前page空闲,没有动画
public static final int SCROLL_STATE_IDLE = 0;
/**
* Indicates that the pager is currently being dragged by the user.
*/
//正在被拖动
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* Indicates that the pager is in the process of settling to a final position.
*/
//正在向最终位置移动
public static final int SCROLL_STATE_SETTLING = 2;
正如onInterceptTouchEvent()注释所说的,方法只是判断我们是否应该拦截这个Touch事件,scrolling都交给onTouchEvent()去做。因为在switch中每一个分支都有break,所以我调换了一下源码的顺序把DOWN放在了MOVE前面,这样更加清晰。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
// Always take care of the touch gesture being complete.
//如果一套手势结束,返回false
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the drag.
if (DEBUG) Log.v(TAG, "Intercept done!");
resetTouch();
return false;
}
// Nothing more to do here if we have decided whether or not we
// are dragging.
if (action != MotionEvent.ACTION_DOWN) {
//如果正在被drag,拦截
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
//不允许drag,不拦截
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
switch (action) {
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();
//获取第一个触摸点的id
mActivePointerId = ev.getPointerId(0);
//设置允许拖拽为false
mIsUnableToDrag = false;
//标记为开始滚动
mIsScrollStarted = true;
//这个Scroller是在initViewPager()中创建的,这里手动调用计算一下 //Scroller中的x,y值
mScroller.computeScrollOffset();
//如果此时正在向终态靠拢,并且离最终位置还有一定距离
if (mScrollState == SCROLL_STATE_SETTLING
&& Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
// Let the user 'catch' the pager as it animates.
//让用户抓住这个Pager
//停止滚动
mScroller.abortAnimation();
//???
mPopulatePending = false;
//更新缓存page信息(滚动的时候mCurItem会改变?)
populate();
//表示在拖动
mIsBeingDragged = true;
//不允许父ViewGroup拦截
requestParentDisallowInterceptTouchEvent(true);
//设置新的状态
setScrollState(SCROLL_STATE_DRAGGING);
} else {
//这里我理解的是正在向终态靠近且距离足够小了,所以不能干涉移动
completeScroll(false);
mIsBeingDragged = false;
}
....
break;
}
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.
*/
//注释的很清楚,如果能进入这个地方说明mIsBeingDragged == false
//这里要检查用户是否已经从原始位置移动的够远,以给mIsBeingDragged赋值
//第一个触摸点的id
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
//不太懂,貌似对第一个触摸点id做了一个转换
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);
...
//这里是说如果在这个区域子View可以滑动,交给子View处理,不拦截
//canScroll的源码贴在后面,在子View中寻找可以滑动的
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;
}
//如果横向偏移绝对值大于最小值 且 yDiff/xDiff < 0.5f
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 MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
...
return mIsBeingDragged;
}
遍历子View,触点在子View的边界内,且子View可以滑动,返回true
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);
}
前面onInterceptTouchEvent()判断了很多情况,基本都是根据情况判断能不能drag,然后给mIsBeingDragged这个变量赋值,代表最终是否拦截接下来的一串手势。在MOVE的末尾,在可以drag的情况下,我们会进入这个方法来让页面跟随手指的手势。
private boolean performDrag(float x) {
boolean needsInvalidate = false;
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
float oldScrollX = getScrollX();
//ViewPager的视图横坐标
float scrollX = oldScrollX + deltaX;
final int width = getClientWidth();
//子View左边界和子View右边界
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);
//如果第一个页面信息不是数据的第0项,更新一下leftBound
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;
}
performDrag()方法让ViewPager的视图滑动了(通过scrollTo()方法),并且调用了这个方法,现在我们来看一下这个方法。从整体上来看,这个方法做了这么几件事:
1.根据视图的scrollX获得了当前的页面信息。
2.计算了视图滑动距离的比例和像素。
3.onPageScrolled(currentPage, pageOffset, offsetPixels)
这里说一下,如果滑动造成ViewPager显示区域内有两个Page可以显示,infoForCurrentScrollPosition()返回的是左边那个的ItemInfo。
private boolean pageScrolled(int xpos) {
//传进来的参数指的是ViewPager视图滑动的距离。
...
//根据scrollX获取当前应该显示的页面信息
final ItemInfo ii = infoForCurrentScrollPosition();
final int width = getClientWidth();
final int widthWithMargin = width + mPageMargin;
final float marginOffset = (float) mPageMargin / width;
//当前页面的position(这个position是指在数据列表中的位置)
final int currentPage = ii.position;
//这里计算的是 当前页面视图滑动距离的比例 和 当前页面宽度比例 的比
//如果第二项是1的话,也就代表了 当前页面视图滑动距离(和页面宽度)的比例
final float pageOffset = (((float) xpos / width) - ii.offset)
/ (ii.widthFactor + marginOffset);
//表示视图滑动的像素
final int offsetPixels = (int) (pageOffset * widthWithMargin);
mCalledSuper = false;
onPageScrolled(currentPage, pageOffset, offsetPixels);
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return true;
}
这个方法在很多滚动的地方都会被调用,我们可以重写这个方法实现一些动画效果。纵观这个方法,做了这么几件事:
1.对DecorView进行滚动
2.回调接口的onPageScrolled(),就是我们自己添加的接口。
3.实现动画
protected void onPageScrolled(int position, float offset, int offsetPixels) {
// Offset any decor views if needed - keep them on-screen at all times.
//1
if (mDecorChildCount > 0) {
final int scrollX = getScrollX();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
final int width = getWidth();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) continue;
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
int childLeft = 0;
switch (hgrav) {
default:
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
paddingLeft += child.getWidth();
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
paddingLeft);
break;
case Gravity.RIGHT:
childLeft = width - paddingRight - child.getMeasuredWidth();
paddingRight += child.getMeasuredWidth();
break;
}
childLeft += scrollX;
final int childOffset = childLeft - child.getLeft();
if (childOffset != 0) {
child.offsetLeftAndRight(childOffset);
}
}
}
//2
dispatchOnPageScrolled(position, offset, offsetPixels);
//3
if (mPageTransformer != null) {
final int scrollX = getScrollX();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.isDecor) continue;
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
mPageTransformer.transformPage(child, transformPos);
}
}
mCalledSuper = true;
}
回过头来小结一下,onInterceptTouchEvent()中会根据不同情况对mIsBeingDragged进行赋值,在MOVE中如果是可滑动的,就调用scrollTo对视图进行滑动形成拖拽效果,接着pageScrolled()中获得当前页面的信息和偏移量传入onPageScrolled(),onPageScrolled()中对Decor View进行位移,回调接口,产生动画。
onInterceptTouchEvent()产生了拖动效果,但主要还是对是否拦截作出判断,接下来我们看一下onTouchEvent()。
纵观整个方法,MOVE中依旧是拖动,而UP的时候会根据 当前页面、当前页面的offset、速度、横向移动距离 计算出下一个应该显示的页面nextPage,接着调用setCurrentItemInternal()产生滑动。
@Override
public boolean onTouchEvent(MotionEvent ev) {
//一些情况的判断,省略
...
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
boolean needsInvalidate = false;
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
//立即将Scroller中的x,y设为终值
mScroller.abortAnimation();
mPopulatePending = false;
//根据mCurIndex更新需要缓存的页面信息
populate();
// Remember where the motion event started
//记录
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
//如果不在drag(这里有可能是因为没有消耗手势的子View,所以返回来让ViewPager处理)
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);
//如果横向偏移足够大 且 横向偏移大于纵向偏移则可以开始drag
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.
//如果可以drag
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(activePointerIndex);
//实现拖动,performDrag上面已经分析过了
needsInvalidate |= performDrag(x);
}
break;
case MotionEvent.ACTION_UP:
//如果是拖动的时候up
if (mIsBeingDragged) {
//计算x速度
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
//ViewPager显示宽度
final int width = getClientWidth();
//视图横向滑动距离
final int scrollX = getScrollX();
//根据scrollX计算出当前的页面信息。
final ItemInfo ii = infoForCurrentScrollPosition();
//边缘占比
final float marginOffset = (float) mPageMargin / width;
//当前页面在数据列表中的位置
final int currentPage = ii.position;
//计算当前页面偏移
final float pageOffset = (((float) scrollX / width) - ii.offset)
/ (ii.widthFactor + marginOffset);
//横向偏移
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(activePointerIndex);
final int totalDelta = (int) (x - mInitialMotionX);
//确定up后终态所在的页面
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
//重要,这里是产生滑动的关键
setCurrentItemInternal(nextPage, true, true, initialVelocity);
needsInvalidate = resetTouch();
}
break;
...
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = ev.getX(index);
//多点触摸,换了另外一个手指过后更新mLastMotionX和mActivePointerId
mLastMotionX = x;
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
//貌似是多点触摸下一个手指抬起了,要更新mLastMotionX
onSecondaryPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
break;
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
}
这个方法的作用是判断应该交给onMeasure()还是scrollToItem()去完成页面的set。
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//一些健壮性判断,省略
...
final int pageLimit = mOffscreenPageLimit;
//有关跳跃性滑动,跳跃的过程中我们不会让涉及到的页面被更新
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
// We are doing a jump by more than one page. To avoid
// glitches, we want to keep all current pages in the view
// until the scroll ends.
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
//还记得我们上一篇开始的时候我们会进入到这个分支,但是这里不会了。
if (mFirstLayout) {
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {//这里我们更新页面信息,并且滑动到目标页面
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
根据是否为smoothScroll来进行不同的滑动,smoothScrollTo()或者直接scrollTo()。
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)));
}
//如果是平滑滑动
if (smoothScroll) {
//后面具体解析
smoothScrollTo(destX, 0, velocity);
//回调接口
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
//回调接口
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
//直接用scrollTo的方式结束滑动
completeScroll(false);
scrollTo(destX, 0);
pageScrolled(destX);
}
}
void smoothScrollTo(int x, int y, int velocity) {
...
//x轴滑动起始位置
int sx;
boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
//如果此时在滚动
if (wasScrolling) {
//更新起始位置
sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
mScroller.abortAnimation();
setScrollingCacheEnabled(false);
} else {
sx = getScrollX();
}
//y轴滑动起始位置
int sy = getScrollY();
int dx = x - sx;
int dy = y - sy;
if (dx == 0 && dy == 0) {
completeScroll(false);
populate();
setScrollState(SCROLL_STATE_IDLE);
return;
}
setScrollingCacheEnabled(true);
setScrollState(SCROLL_STATE_SETTLING);
final int width = getClientWidth();
final int halfWidth = width / 2;
//滑动距离,距离影响时间
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
final float distance = halfWidth + halfWidth
* distanceInfluenceForSnapDuration(distanceRatio);
//滑动时间
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
duration = (int) ((pageDelta + 1) * 100);
}
duration = Math.min(duration, MAX_SETTLE_DURATION);
// Reset the "scroll started" flag. It will be flipped to true in all places
// where we call computeScrollOffset().
mIsScrollStarted = false;
//重要的地方,就是利用Scroller产生弹性滑动
mScroller.startScroll(sx, sy, dx, dy, duration);
//重绘以便回调
ViewCompat.postInvalidateOnAnimation(this);
}
通过前面的分析我们知道了ViewPager是利用Scroller产生谈性滑动,Scroller产生弹性滑动的关键在于onDraw()中会回调computeScroll(),然后在这个方法里用scrollTo()滑动并再次申请重绘。ViewPager重写了这个方法,在调用scrollTo()之后还调用了pageScrolled(x)对Decor View进行更新、回调接口、产生动画。之后申请重绘。
@Override
public void computeScroll() {
mIsScrollStarted = true;
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
if (!pageScrolled(x)) {
mScroller.abortAnimation();
scrollTo(0, y);
}
}
// Keep on drawing until the animation has finished.
ViewCompat.postInvalidateOnAnimation(this);
return;
}
// Done with scroll, clean up state.
completeScroll(true);
}
ViewPager的拖动和滑动都看完了,onInterceptTouchEvent()和onTouchEvent()的Move中都会对drag进行响应,通过scrollTo方法形成视图的移动,期间通过pageScrolled()完成相关事情的处理,包括Decor View、接口方法回调、动画;onTouchEvent()的Up中可能会产生平滑滑动,利用的是初始化时候定义的Scroller。
到这里对ViewPager的视图的移动流程就有了一个整体上的了解,一些细节我们就可以很快的定位了,接下来一篇想看一下ViewPager和Fragment的交互。