自定义无限循环ViewPager分成以下三篇文章进行讲解:
- ViewPager初始化源码解析
- ViewPager滑动原理解析
- ViewPager方法改造实现无限循环
在前面一篇文章中,已经分析了ViewPager
初始化的原理,而本篇文章开始分析ViewPager
的滑动及页面切换的原理。在阅读本文之前,大家可以先去了解下Scroller
的用法,以便大家更好的理解ViewPager
的滑动原理。
关于ViewGroup
的事件处理不外乎与dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
这三个方法有关,ViewPager
重写了后面两个方法。而ViewPager
根据手势产生页面移动也正是因为重写了这两个方法。ViewPager
存在两种移动方式:
- 在MOVE触摸事件中,页面随手指的拖动而移动。
- 在UP事件后,页面滑动到指定页面(通过Scroller实现的)。
现在,我们先来看下onInterceptTouchEvent()
方法。
onInterceptTouchEvent()
onInterceptTouchEvent()
方法只是判断是否应该拦截这个触摸事件,如果返回true,则将事件交给onTouchEvent()
进行滚动处理。在分析onInterceptTouchEvent()
前,先介绍下页面的三个状态:
public static final int SCROLL_STATE_IDLE = 0;空闲状态
public static final int SCROLL_STATE_DRAGGING = 1;正在被拖拽的状态
public static final int SCROLL_STATE_SETTLING = 2;正在向最终位置移动的状态
public boolean onInterceptTouchEvent(MotionEvent ev) {
//触摸动作
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
// 如果事件取消或者触摸事件结束,返回false,不用拦截事件
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
//重置触摸相关变量
resetTouch();
return false;
}
// 如果当前不是按下事件,就判断一下,是否是在拖拽切换页面
if (action != MotionEvent.ACTION_DOWN) {
//如果正在被拖拽,拦截
if (mIsBeingDragged) {
return true;
}
//不允许拖拽,不拦截
if (mIsUnableToDrag) {
return false;
}
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
// 记录按下触摸的位置。
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
//获取第一个触摸点的id
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
//重置允许拖拽切换页面
mIsUnableToDrag = false;
//计算滑动的偏移量 ,Scroller在初始化initViewPager()中创建
mScroller.computeScrollOffset();
//如果页面此时正在向最终位置移动并且离最终位置还有一定距离时
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
//让用户感觉抓住了这个页面
//所以停止移动
mScroller.abortAnimation();
mPopulatePending = false;
//更新缓存page信息
populate();
//设置为正在拖拽
mIsBeingDragged = true;
//ViewPager向父View申请不要拦截自己的触摸事件
requestParentDisallowInterceptTouchEvent(true);
//设为拖拽状态
setScrollState(SCROLL_STATE_DRAGGING);
} else {
//如果离最终的距离足够小,结束滚动
completeScroll(false);
mIsBeingDragged = false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
//mIsBeingDragged == false, 否则事件已经在上面就被拦截
//第一个触摸点的id,为了处理多点触摸
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// 如果不是有效的触摸id,直接break,不做任何处理
break;
}
//根据第一个触摸点的id获取触摸点的序号
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
//根据这个序号,获取这个触摸点的横坐标
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);
//判断当前显示的子view是否可以滑动,如果可以滑动,交给子view处理,不拦截
//isGutterDrag是判断是否在两个子view之间的缝隙间滑动
//canScroll是判断子view是否可以滑动
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
mLastMotionX = x;
mLastMotionY = y;
//标记ViewPager不去拦截事件
mIsUnableToDrag = true;
return false;
}
//如果水平方向移动绝对值大于最小距离, 且 yDiff/xDiff < 0.5f,表示在水平方向移动
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
//说明正在拖拽
mIsBeingDragged = true;
//ViewPager向父View申请不要拦截自己的触摸事件
requestParentDisallowInterceptTouchEvent(true);
//设置为拖拽的状态
setScrollState(SCROLL_STATE_DRAGGING);
//保存当前位置
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
//启用缓存
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
// 如果是垂直方向上的移动则不拦截
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
//如果在拖拽,让子view跟随手指进行移动
//performDrag(x)方法很重要,下面会有详细介绍
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
//此方法用于处理多点触摸,如果抬起的是第一个触摸点,则将mActivePointerId设为第二个触摸点
onSecondaryPointerUp(ev);
break;
}
//添加速度追踪类
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
return mIsBeingDragged;
}
onInterceptTouchEvent()
主要作用就是判断各种情况是不是在拖拽,是否要拦截此事件。在MOVE事件中,如果在拖拽,会调用performDrag()
方法让当前页面移动。下面便分析此此方法。
performDrag()
private boolean performDrag(float x) {
boolean needsInvalidate = false;
//两次MOVE的移动距离
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
//viewpager滚动的距离
float oldScrollX = getScrollX();
//viewpager需要滚动的距离
float scrollX = oldScrollX + deltaX;
final int width = getClientWidth();
//子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);
//如果第一个缓存页面不是adapter中第一个页面,更新子view的左边界
if (firstItem.position != 0) {
leftAbsolute = false;
leftBound = firstItem.offset * width;
}
//如果最后一个缓存页面不是adapter中最后一个页面,更新子view的右边界
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;
}
mLastMotionX += scrollX - (int) scrollX;
//滚动到相应的位置
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
return needsInvalidate;
}
performDrag()
方法做了这么几件事:首先得到viewpager需要滚动的距离,其次得到边界条件leftBound
和rightBound
,根据边界条件的约束得到真正的滚动距离,最后调用scrollTo()
方法滚动到最终的位置。简单来说,performDrag()
方法让ViewPager的视图滑动了。紧接着,再看看pageScrolled()
方法到底做了那些操作。
pageScrolled()
private boolean pageScrolled(int xpos) {
//如果没有任何的页面缓存信息
if (mItems.size() == 0) {
//mCalledSuper作用是:如果子类重写了onPageScrolled,
// 那么子类的实现必须要先调用父类ViewPager的onPageScrolled
//为了确保子类的实现中先调用了父类ViewPager的onPageScrolled,定义了mCalledSuper
//并且在ViewPager类中的onPageScrolled将mCalledSuper设置为了true,用于判断子类有没有调用。
mCalledSuper = false;
onPageScrolled(0, 0, 0);
//如果没有执行ViewPager的onPageScrolled,抛出异常
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return false;
}
//根据当前滑动的位置,得到当前显示的子view的页面信息iteminfo
final ItemInfo ii = infoForCurrentScrollPosition();
final int width = getClientWidth();
final int widthWithMargin = width + mPageMargin;
final float marginOffset = (float) mPageMargin / width;
//当前页面的position,在adapter数据中的位置
final int currentPage = ii.position;
//得到当前页面的偏移量
final float pageOffset = (((float) xpos / width) - ii.offset) /
(ii.widthFactor + marginOffset);
//当前页面偏移的像素值
final int offsetPixels = (int) (pageOffset * widthWithMargin);
//以下几句代码跟上面的作用一样,都是如果子类重写了onPageScrolled,必须要先调用ViewPager的onPageScrolled
mCalledSuper = false;
onPageScrolled(currentPage, pageOffset, offsetPixels);
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return true;
}
pageScrolled(int xpos)
简单来说就是根据当前的滑动位置,找到当前的页面信息,然后得到viewpager滑动距离,最后调用了onPageScrolled(currentPage, pageOffset, offsetPixels)
,此方法下面会有分析。值得注意的是,infoForCurrentScrollPosition()
是符合找到当前显示的页面的?
private ItemInfo infoForCurrentScrollPosition() {
final int width = getClientWidth();
//viewpager滑动距离比例
final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0;
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
int lastPos = -1;
float lastOffset = 0.f;
float lastWidth = 0.f;
boolean first = true;
ItemInfo lastItem = null;
// 遍历所有预存的页面
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
float offset;
//第一次first=true,不进入;第二次判断mitems中缓存的页面是不是有丢失
if (!first && ii.position != lastPos + 1) {
// Create a synthetic item for a missing page.
ii = mTempItem;
ii.offset = lastOffset + lastWidth + marginOffset;
ii.position = lastPos + 1;
ii.widthFactor = mAdapter.getPageWidth(ii.position);
i--;
}
offset = ii.offset;
//根据页面获取左右边界,然后通过与滑动比例的比较,找到当前显示的页面
final float leftBound = offset;
final float rightBound = offset + ii.widthFactor + marginOffset;
if (first || scrollOffset >= leftBound) {
if (scrollOffset < rightBound || i == mItems.size() - 1) {
return ii;
}
} else {
return lastItem;
}
first = false;
//存储检查过的页面信息
lastPos = ii.position;
lastOffset = offset;
lastWidth = ii.widthFactor;
lastItem = ii;
}
return lastItem;
}
通过上面源码的分析,首先获得viewpager滑动过的距离比例,然后通过遍历mItems
缓存列表,根据每个缓存页面的offset值得到改页面的左右边界,最后就是判断viewpager滑动过的距离比例在哪一个缓存页面的边界之内,这个缓存页面就是当前显示的页面。而如果viewpager显示区域内存在两个页面显示的时候,从缓存列表的遍历顺序就可以看出,返回的必然是最左边的页面。
onPageScrolled()
从上面的代码分析可以看出,pageScrolled()
方法只是为了调用onPageScrolled()
做传参的计算。其中,
position
表示当前显示页面的位置
offset
当前页面位置的偏移
offsetPixels
当前页面偏移的像素大小。
protected void onPageScrolled(int position, float offset, int offsetPixels) {
//如果有DecorView,则需要使得它们一直显示在屏幕中,不移出屏幕
if (mDecorChildCount > 0) {
//根据Gravity将DecorView摆放到指定位置。可参考onMeasure的分析
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);
}
}
}
//分发页面滚动事件,即回调监听的onPageScrolled方法
dispatchOnPageScrolled(position, offset, offsetPixels);
//如果mPageTransformer不为null,则不断去调用mPageTransformer的transformPage函数
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;
//子view的位置
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
//回到transformPage方法
mPageTransformer.transformPage(child, transformPos);
}
}
mCalledSuper = true;
}
上面代码中回调transformPage()
中的transformPos
的取值范围如下:
(-∞ , -1) :表示左边 的View 且已经看不到了
[-1 , 0] :表示左边的 View ,且可以看见
( 0 , 1] :表示右边的VIew , 且可以看见了
( 1 , -∞) : 表示右边的 View 且已经看不见了
举个栗子:
如果a 是第一页,b 是第二页
当前页为 a, 当 a 向左滑动, 直到滑到 b 时:
a 的position变化是 [-1 , 0] 由 0 慢慢变到 -1
b 的position变化是 ( 0 , 1] 由 1 慢慢变到 0当前页为b, 当 b 向右滑动, 直到滑到a 时:
a 的position变化是 [-1 , 0] 由 -1 慢慢变到 0
b 的position变化是 ( 0 , 1] 由 0 慢慢变到 1
onPageScrolled()
方法就分析到这里,它其实就做了三件事:
- 将DecorView显示在屏幕中,不移除屏幕
- 回调接口的
onPageScrolled()
方法 - 回调接口的
transformPage()
方法,自定义实现页面转换动画
基本上到这里,onInterceptTouchEvent()
流程中涉及的方法就分析完毕了。简单总结下,就是在onInterceptTouchEvent()
方法中根据不同情况对mIsBeingDragged
进行赋值,对触摸事件是否进行拦截;如果在MOVE事件中是可滑动的,就调用performDrag()
让视图跟着滑动,当然此方法中是调用scrollTo()
方法形成拖拽效果,接着调用pageScrolled()
方法,获取得当前页面的信息和偏移量传入onPageScrolled()
方法,再在onPageScrolled()
中对DecorView固定显示,回调接口,回调转换动画接口。
虽然,onInterceptTouchEvent()
中产生了拖动效果,但主要还是对是否拦截事件作出判断,关于页面的滑动还是在onTouchEvent()
中进行处理。
onTouchEvent()
public boolean onTouchEvent(MotionEvent ev) {
if (mFakeDragging) {
//使用程序模拟拖拽事件,忽略真正的触摸事件
return true;
}
if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
// 不立即处理边缘触摸事件
return false;
}
if (mAdapter == null || mAdapter.getCount() == 0) {
//mAdapter中数据为空,不处理事件
return false;
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
//添加速度追踪
mVelocityTracker.addMovement(ev);
//获取触摸事件
final int action = ev.getAction();
boolean needsInvalidate = false;
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
//如果按钮,立即停止滚动
mScroller.abortAnimation();
mPopulatePending = false;
//根据mCurIndex更新需要缓存的页面信息
populate();
// 保存起始触摸点
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEvent.ACTION_MOVE:
//如果不在drag(这里有可能是因为没有消耗手势的子View,返回来让ViewPager处理)
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex == -1) {
// A child has consumed some touch events and put us into an inconsistent state.
needsInvalidate = resetTouch();
break;
}
//计算x和y方向的移动
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
// 如果x方向移动足够大,且大于y方向的移动,则开始拖拽
if (xDiff > mTouchSlop && xDiff > yDiff) {
mIsBeingDragged = true;
//ViewPager向父View申请不要拦截自己的触摸事件
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
//设置为拖拽的状态
setScrollState(SCROLL_STATE_DRAGGING);
//启用缓存
setScrollingCacheEnabled(true);
// 感觉和 requestParentDisallowInterceptTouchEvent(true)重复了
//就是requestParentDisallowInterceptTouchEvent方法的具体实现
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
// 在拖拽的状态
if (mIsBeingDragged) {
// 调用performDrag(),实现页面的滑动,上面已经分析过了
final int activePointerIndex = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
needsInvalidate |= performDrag(x);
}
break;
case MotionEvent.ACTION_UP:
//如果是在拖拽状态抬起手指
if (mIsBeingDragged) {
//计算x方向的滑动速度
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int width = getClientWidth();
//获取viewpager横向滑动距离
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);
// 滑动到 nextPage 页
setCurrentItemInternal(nextPage, true, true, initialVelocity);
needsInvalidate = resetTouch();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
//滑动到当前的页面
scrollToItem(mCurItem, true, 0, false);
needsInvalidate = resetTouch();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, index);
mLastMotionX = x;
//多点触摸,换了另外一个手指过后更新mLastMotionX和mActivePointerId
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
//多点触摸下一个手指抬起了,要更新mLastMotionX
onSecondaryPointerUp(ev);
mLastMotionX = MotionEventCompat.getX(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
break;
}
if (needsInvalidate) {
//如果需要重绘,重绘viewpager
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
}
纵观整个方法,MOVE中调用performDrag()
实现拖动,而UP的时候则根据计算出下一个应该显示的页面nextPage
,接着调用setCurrentItemInternal()
产生滑动。
关于onTouchEvent()
方法的代码与onInterceptTouchEvent()
有很多的相似之处,如果对onInterceptTouchEvent()
有所理解的话,相信对onTouchEvent()
的理解也会比较简单的。不过,在onTouchEvent()
方法中关于抬起事件和事件取消中,调用了determineTargetPage()
、setCurrentItemInternal()
和scrollToItem()
这三个方法。至于scrollToItem()
方法,在上篇文章ViewPager初始化源码解析已经有过分析,其作用就是滑动mCurItem
的目标页面。至于前两个方法,下面会一一进行讲解。
determineTargetPage()
determineTargetPage()
方法通过滑动速度,滑动距离以及当前页面位置偏移计算出下一个页面的position。
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
int targetPage;
//如果滑动的距离大于最小的飞速滚动距离,且滑动速度大于最小的飞速滑动速度
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
//如果速度大于0,下个页面position则为当前显示页面的位置+1
targetPage = velocity > 0 ? currentPage : currentPage + 1;
} else {
//如果滑动距离和滑动速度均不满足最小要求,则通过当前显示页面的偏移得到下个页面
final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
targetPage = (int) (currentPage + pageOffset + truncator);
}
if (mItems.size() > 0) {
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
// 最后进行边界判断取值,下个页面的position在缓存页面的position之间
targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
}
return targetPage;
}
setCurrentItemInternal()
viewpager可以调用setCurrentItem(int item)
选中需要显示的页面,此方法最后也是调用的是setCurrentItemInternal()
方法。
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
if (mAdapter == null || mAdapter.getCount() <= 0) {
setScrollingCacheEnabled(false);
return;
}
if (!always && mCurItem == item && mItems.size() != 0) {
setScrollingCacheEnabled(false);
return;
}
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
//以上代码都是一些代码健壮性检查,如果不满足条件,直接返回
//缓存页面的数量
final int pageLimit = mOffscreenPageLimit;
//如果需要显示的页面超过了需要缓存的页面,将所有缓存页面的滚动状态设为true
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
for (int i=0; i
如果不是第一次布局,mFirstLayout=false,则会执行scrollToItem()
方法,虽然此方法在上篇文章中有作分析,为了能连贯阅读这里在贴下源码。
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();
// 获取 item 的水平 方向的offset偏移值
destX = (int) (width * Math.max(mFirstOffset,
Math.min(curInfo.offset, mLastOffset)));
}
if (smoothScroll) {
// 平滑滚动到偏移位置
smoothScrollTo(destX, 0, velocity);
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
//是否需要分发OnPageSelected回调
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
//滑动结束后的清理复位
completeScroll(false);
//滚动到偏移的位置,结束滑动
scrollTo(destX, 0);
//最后会调用onPageScrolled(currentPage, pageOffset, offsetPixels)方法
pageScrolled(destX);
}
}
滑动到目标页面存在两种方式,一种是平滑滑动到目标页面,一种是直接滑动动目标位置。如果是onTouchEvent()
的Up事件滑动到目标页面则是第一种,而初始化完成之后通过调用setCurrentItem(int item)
滑动到目标页面则是第二种。我们先看下,smoothScroll=true
的平滑滑动的过程。
void smoothScrollTo(int x, int y, int velocity) {
//没有子view直接返回
if (getChildCount() == 0) {
setScrollingCacheEnabled(false);
return;
}
//获取viewpager滚动的距离
int sx = getScrollX();
int sy = getScrollY();
//需要滚动的距离
int dx = x - sx;
int dy = y - sy;
//如果需要滚动的距离为0,结束滚动,更新页面信息,设置空闲的滚动状态
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);
//smoothScrollTo并没有使用匀速滑动,而是通过distanceInfluenceForSnapDuration函数来实现变速,
final float distance = halfWidth + halfWidth *
distanceInfluenceForSnapDuration(distanceRatio);
int duration = 0;
velocity = Math.abs(velocity);
if (velocity > 0) {
//如果手指滑动速度不为0,根据手指滑动速度计算滑动持续时间
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
//如果手指滑动速度为0,即通过代码的方式滑动到指定位置,则使用下面的方式计算滑动持续时间
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);
//用scroller类开始平滑滑动
mScroller.startScroll(sx, sy, dx, dy, duration);
//重绘
ViewCompat.postInvalidateOnAnimation(this);
}
在上面的代码里mScroller.startScroll()
开启了平滑滑动后,会不断的调用computeScroll()
方法,然后重写此方法,完成视图的滑动。
public void computeScroll() {
//确保mScroller还没有结束计算滑动位置
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
//获取当前所处的位置oldX,oldY
int oldX = getScrollX();
int oldY = getScrollY();
//获取mScroller计算出来的位置
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
//只要x和y方向有一个发生了变化,就去滑动
if (oldX != x || oldY != y) {
//滑到mScroller计算出来的新位置
scrollTo(x, y);
//调用pageScrolled,此方法上面有分析,只有当ViewPager里面没有子View才会返回false
if (!pageScrolled(x)) {
//结束动画,并使得当前位置处于最终的位置
mScroller.abortAnimation();
//没有子View,说明x方向无需滑动,再次确保y方向滑动
scrollTo(0, y);
}
}
//不断的postInvalidate,不断重绘,达到动画效果
ViewCompat.postInvalidateOnAnimation(this);
return;
}
//如果滑动结束了,做一些结束后的清理相关操作
completeScroll(true);
}
Viewpager利用Scroller产生平滑滑动,其关键点在于启动滑动后,会不断回调computeScroll()
,ViewPager重写了这个方法,然后调用scrollTo()
滑动之后还调用了pageScrolled(x)
对DecorView进行位置更新、回调接口、产生动画,最后申请重绘。
在computeScroll()
方法的最后,如果滑动结束了,调用了completeScroll(true)
方法,此方法在很多地方都用调用,我们来看下它究竟做了那些操作。
private void completeScroll(boolean postEvents) {
//如果当前滑动状态是SCROLL_STATE_SETTLING
boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
if (needPopulate) {
setScrollingCacheEnabled(false);
//停止滑动
mScroller.abortAnimation();
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
//如果还没滑动到目标位置,调用 scrollTo()确保滑动最终的位置
if (oldX != x || oldY != y) {
scrollTo(x, y);
if (x != oldX) {
pageScrolled(x);
}
}
}
mPopulatePending = false;
//将缓存页面的滚动状态设为false
for (int i=0; i
小结
关于ViewPager的滑动以及页面切换的原理分析就到此结束了,关于ViewPager的两种移动方式所涉及到的相关方法也都有分析到,
- 其中在
onInterceptTouchEvent()
和onTouchEvent()
的MOVE事件中,调用performDrag()
对拖拽进行处理,通过scrollTo()
方法完成页面的移动,期间通过pageScrolled()
完成相关事情的处理,如DecorView显示、接口方法回调、动画接口回调等; - 而另外一种移动方式在
onTouchEvent()
的UP事件中,调用setCurrentItemInternal()
对平滑滑动进行处理,通过最后调用smoothScrollTo()
方法,利用Scroller达到目的,当然最后也调用了pageScrolled()
进行接口的回调等操作,在滑动结束的最后,调用completeScroll(boolean postEvents)
完成滑动结束后的相关清理工作。
最后
关于改造ViewPager变为无限循环的第二部分(ViewPager滑动原理解析)所有内容都已分析完毕了,只剩下最后一部分ViewPager方法的改造了,最后一篇文章也会尽快发布出来。如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!