版本
android.support.v4.view.ViewPager
android.support.v4.view. PagerAdapter
这一篇主要是针对ViewPager的深度解析,本篇文章就为了解决两个疑问:ViewPager是怎么实现的?ViewPager与PagerAdapter又是怎么交互的?我们会一一揭开ViewPager的面纱,借此对ViewPager有更深层次的了解,而不仅仅停留在使用层面上。本篇通过以下三个问题来剖析ViewPager源码:
如果你有接触过自定义View的话,那么你会知道我们通常都是通过重写View.onMeasure(),View.onLayout(),View.onDraw()。去自定义View.所以我们通过这三个方法去一一剖析。
在分析解析onMeasure之前我们要先明白一点,ViewPager的Child View分为两种,一种是Decor View
装饰视图,一种是Content View,也就是我们通过适配器给予的内容视图。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置ViewPager的测量大小,这里只是通过getDefaultSize简单获取并设置了整个ViewPager,这里获取的值实质是父容器给予的最大值。
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
/**. 省略Code..**/
//处理Padding。
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() -getPaddingBottom();
//确定Decor View的大小,并减去相应的空间,给予Content View一个剩余可用大小.
int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
/**..省略Code..***/
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
//测量Decor View
child.measure(widthSpec, heightSpec);
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
//创建剩余空间的MeasureSpec
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
mInLayout = true;
//更新Content View.
//这是一个源码中十分重要的方法,下面会具体分析。
populate();
mInLayout = false;
// 测量Content View大小.
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
//lp.widthFactor起了什么作用?
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
//测量Content View,mChildHeightMeasureSpec就是上面减去装饰View后的剩余大小
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
上面我们提到了populate()方法。这个方法主要是更新&创建&销毁相应的Content View,这个方法在源码中多次被使用,这个方法也确实起着十分重要的作用,我们看看它的源码。
void populate() {
//调用另一个重载方法,直接看下面的重载方法
populate(mCurItem);
}
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
int focusDirection = View.FOCUS_FORWARD;
//更新当前项的位置变量
if (mCurItem != newCurrentItem) {
focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
/**..省略Code...,这里是一系列针对强壮性的判断,自行查看源码**/
//PagerAdapter回调
mAdapter.startUpdate(this);
//mOffscreenPagLimit这个常量代表着ViewPager会保持着多少个实例
final int pageLimit = mOffscreenPageLimit;
final int startPos = Math.max(0, mCurItem - pageLimit);
final int N = mAdapter.getCount();
final int endPos = Math.min(N-1, mCurItem + pageLimit);
/**..省略Code...,这里是一系列针对强壮性的判断,自行查看源码**/
int curIndex = -1;
ItemInfo curItem = null;
//获取Content View相应的ItemInfo,ItemInfo保存着Content View的引用以及位置等重要信息
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
//如果没有当前位置的Content View信息,明显我们需要去创建它,这里调用了addNewItem去创建一个新的Content View。
//addNewItem就是通过PagerApdate.instantiateItem()方法获取新Content View
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
//这里就是ViewPager固定持有一定页面的具体逻辑代码
if (curItem != null) {
/** ..省略Code.. **/
for (int pos = mCurItem - 1; pos >= 0; pos--) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
/** ..省略Code..,根据页面最大持有数创建和销毁左边页面Content View **/
}
}
float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.f) {
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
for (int pos = mCurItem + 1; pos < N; pos++) {
/** ..省略Code..,根据页面最大持有数创建和销毁右边页面Content View **/
}
}
//更新页面的偏移量,主要是ItemInfo里面的信息
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
//PagerAdapter方法回调
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
mAdapter.finishUpdate(this);
/**..省略Code..,更新所有 Content View的Layout Params信息**/
}
这里我们总结一下populate(int)做的事情:
1.如果还未有当前页面的Content View,则通过PagerAdapter创建 Content View。
2.根据页面数量极限值,即ViewPager.mOffscreenPageLimit.来创建或者销毁页面左右的两边的页面。
3.更新所有Content View 的LayoutParams属性
结束了populate(int)这个小插曲之后,接下来我们分析 onLayout()。在onLayout过程中,主要是处理Decor View 与Content View的位置,由于Decor View 可能有多个,所以这里是根据类似线性布局的方式对Decor View进行布局。例如从上到下等,Content View 则在减去了Decor View占用空间的基础上,从左到右进行布局。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
int width = r - l;
int height = b - t;
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
final int scrollX = getScrollX();
int decorCount = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = 0;
int childTop = 0;
if (lp.isDecor) {
/**省略Code,根据装饰视图的布局进行相应处理**/
}
childLeft += scrollX;
//调用Decor View 布局方法
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
decorCount++;
}
}
}
//其中padding的值已附加上Decor View所占有的位置
final int childWidth = width - paddingLeft - paddingRight;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
if (lp.needsMeasure) {
lp.needsMeasure = false;
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidth * lp.widthFactor),
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (height - paddingTop - paddingBottom),
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
//调用了Content View的布局方法,注意这里的padding值已减去了Decor View占有的位置
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
/***省略Code,保存padding值**/
//处理当前滚动信息
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
}
onLayout()方法十分清晰,主要是对Decor View 与Content View的布局。
接下来我们看绘画阶段,ViewPager的重写了draw(Canvas)与 onDraw(Canvas)。draw(Canvas)方法很简单,只是对左右边缘效果进行处理,而onDraw(Canvas)亦十分简单,只是绘画了页面的间隔效果。
public void draw(Canvas canvas) {
super.draw(canvas);
boolean needsInvalidate = false;
final int overScrollMode = ViewCompat.getOverScrollMode(this);
if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
(overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
mAdapter != null && mAdapter.getCount() > 1)) {
if (!mLeftEdge.isFinished()) {
/**省略Code,这里绘画左边缘效果**/
}
if (!mRightEdge.isFinished()) {
/**省略Code,这里绘画右边缘效果**/
}
} else {
mLeftEdge.finish();
mRightEdge.finish();
}
if (needsInvalidate) {
// Keep animating
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the margin drawable between pages if needed.
if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
/**省略Code,根据注释这里绘画了页面的间隔效果**/
}
}
}
至此,ViewPager的测量、布局、绘画三个过程均已经解析完毕。
在这三个过程中,不知道大家在阅读源码的时候会不会对Child View的来源感产生疑问?这里我特别根据ViewGroup.addView在ViewPager里面进行关键字搜索,并没有找到相应添加View的方法,所以这也是为什么在ViewPager.instantiateItem时候需要手动将Content View添加到ViewPager的原因,对应的,我们需要在ViewPager.destoryItem中将Content View从ViewPager移除。
Android中滚动具体有两种:一种是拖动Drag,一种是滑动Fling.ViewPager均实现了这两种滚动方式。
如果你对View的事件传递有一定的了解的话,那么你肯定会知道我们对View触摸事件的处理集中在以下三个方法中:
由于ViewPager只重写了onTouchEvent,所以我们直接从onTouchEvent去分析ViewPager.我们直接从MotionEvent的几个原子操作来一一分析ViewPager的滚动。
@Override
public boolean onTouchEvent(MotionEvent ev) {
//这里开始直到switch部分做了了一些健壮性的判断和参数初始化
if (mFakeDragging) {return true; }
if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
return false;
}
if (mAdapter == null || mAdapter.getCount() == 0) {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: {
/**..省略Code..,下面会分析**/
break;
}
case MotionEvent.ACTION_MOVE:
/**..省略Code..,下面会分析**/
break;
case MotionEvent.ACTION_UP:
/**..省略Code..,下面会分析**/
break;
case MotionEvent.ACTION_CANCEL:
/**..省略Code..,下面会分析**/
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
/**..省略Code..,下面会分析**/
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
/**..省略Code..,下面会分析**/
break;
}
//重绘,更新界面
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
}
接下来我们分析各个ACTION动作,这里强调一下ViewPager拖动的逻辑在ACTION_MOVE里面,滑动的逻辑在ACTION_UP里面。
MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_DOWN: {
//停止目前的动画
mScroller.abortAnimation();
mPopulatePending = false;
//更新Content View
populate();
//更新最新触摸坐标值,这两个值十分重要
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
//需要记录当前有效的触摸手指ID,用于跟踪该手指的触摸情况
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
ACTION_DWON里面主要是记录起始的坐标值与有效的触摸手指,为后续的动作提供数据。ViewPager支持多点触摸,所以我们接着看多触摸相关的ACTION_PONTER_DOWN和ACTION_POINTER_UP。
MotionEvent.ACTION_POINTER_DOWN&MotionEvent.ACTION_POINTER_UP
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, index);
//上面ACTION_DOWN就是记录着两个关键参数
//这里更换了另外一个触摸手指,所以需要更新最新X坐标与触摸手指ID
mLastMotionX = x;
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
//重新设置当前触摸手指并且更新最新X坐标
onSecondaryPointerUp(ev);
mLastMotionX = MotionEventCompat.getX(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
break;
MotionEvent.ACTION_POINTER_DOWN&MotionEvent.ACTION_POINTER_UP跟ACTION_DOWN的处理逻辑其实类似,均是记录更新当前的最新的X坐标与有效的触摸手指ID。
接下来重头戏来了,我们先看看ViewPager的拖动是怎么实现的
MotionEvent.ACTION_MOVE
case MotionEvent.ACTION_MOVE:
//未进入拖动状态,不做任何处理
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
//健壮性判断
if (pointerIndex == -1) {
needsInvalidate = resetTouch();
break;
}
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);
//根据TouchSlop判断是否是拖动动作
if (xDiff > mTouchSlop && xDiff > yDiff) {
//设置拖动标志位
mIsBeingDragged = true;
//拦截父View触摸事件
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
//设置当前的滚动状态
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// 再次拦截,以防万一
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
//如果上面判别为拖动,这里则会执行拖动的实现方法
if (mIsBeingDragged) {
final int activePointerIndex = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
//拖动的实现在此,看下面分析
needsInvalidate |= performDrag(x);
}
break;
我们接着看拖动的实现方法,performDrag(float)方法。
private boolean performDrag(float x) {
boolean needsInvalidate = false;
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
float oldScrollX = getScrollX();
float scrollX = oldScrollX + deltaX;
/**..省略Code..,scrollX的具体计算逻辑**/
// 更新最新X坐标
mLastMotionX += scrollX - (int) scrollX;
//这里就执行了拖动,这个方法就是十分常用的滚动方法
scrollTo((int) scrollX, getScrollY());
/**pageScrolled函数主要做了两件事: 1、对Decor View 进行同步拖动 2、如果ViewPager自定义了页面动画,则会对自定义的过度动画进行处理 **/
pageScrolled((int) scrollX);
return needsInvalidate;
}
由此可见ViewPager最终通过scrollTo来实现拖动。我们接着看滑动实现。
MotionEvent.ACTION_UP
由于ACTION_UP较为复杂,所以我们先大致说下ACTION_UP做的事情:
1、确定最后滑动到的页面。我们在使用ViewPager时候,我们会发现如果拖动的页面距离不够,它会弹回原页面,逻辑就是在这里。
2、更新Content View
3、滑动到相应的页面,如果是滑动效果,则通过Scroller滑动,否则通过scrollTo滑动。
下面我们看具体代码。
case MotionEvent.ACTION_UP:
// 如果是拖动状态才会去执行滑动
if (mIsBeingDragged) {
//这里到detemineTargetPager函数之前均是计算滑动所需要的参数
/** 包括currentPage-->当前页面 pageOffset-->页面偏移量, initalVelocity-->X轴滑动的速度 totalDelta-->与开始滑动时候相比较的滑动偏移量 **/
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 int =determineTargetPage(currentPage,pageOffset,
//滑动实现的方法,下面具体分析
setCurrentItemInternal(nextPage, true, true, initialVelocity);
//停止拖动并且,重新初始化部分重要变量,包括触摸手指等。
mActivePointerId = INVALID_POINTER;
endDrag();
needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
}
break;
我们接着看滑动实现的具体方法,setCurrentItemInternal(int,boolean ,boolean,int)方法。
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
/**..省略Code...,健壮性判断**/
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
//这里是针对跳跃性页面的处理,例如我从0页面直接滑动到4页面。
final int pageLimit = mOffscreenPageLimit;
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
for (int i=0; i<mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
mCurItem = item;
/**..省略Code...,相应回调调用,自行阅读源码**/
requestLayout();
} else {
//更新Content View
populate(item);
//通过Scroller执行平滑滚动,或者通过scroll直接滚动,具体看下面分析
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
最终执行滑动的方法是scrollToItem(int , boolean , int ,boolean ) 。话不多说,直接上代码。
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)));
}
//smoothScroll决定是否调用Scroller平滑的滑动
if (smoothScroll) {
//调用Scoller平滑滑动
smoothScrollTo(destX, 0, velocity);
//这里就是我们经常监听的PagerSelected方法
if (dispatchSelected && mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(item);
}
if (dispatchSelected && mInternalPageChangeListener != null) {
mInternalPageChangeListener.onPageSelected(item);
}
} else {
//首先调用PagerSelected回调方法
if (dispatchSelected && mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(item);
}
if (dispatchSelected && mInternalPageChangeListener != null) {
mInternalPageChangeListener.onPageSelected(item);
}
//通过scrollTo滑动页面
completeScroll(false);
scrollTo(destX, 0);
//这个方法在ACTION_MOVE里面亦有作说明,这里不再重复
pageScrolled(destX);
}
}
ViewPager的整个滚动实现过程就是这样了,这里再做简要的总结:
MotionEvent.ACTION_DOWN&MotionEvent.POINTER_DOWN&MotionEvent.POINTER_UP:主要更新了当前的触摸手指ID和最新的X坐标。
MotionEvent.ACTION_UP:处理滑动。
MotionEvent.ACTION_MOVE:处理拖动。
ViewPager需要PagerAdapter才能够实现相应的功能,而我们创建PagerAdapter的时候需要重写一些方法,才能使得ViewPager正常运作,所以这里我们这里主要看看这些方法与ViewPager的交互:
总共4个方法来分析PagerAdapter与ViewPager的交互。
instantiateItem(ViewGroup,int)
instantiateItem(ViewGroup,int)方法在ViewPager.addNewItem(int, int)中被调用,主要作用是创建一个页面的Content View。这里再强调一遍,在重写这个方法时候,需要主动将该Content View添加到ViewPager里面去,因为ViewPager没有主动添加该Content View为其Child View。
destroyItem(ViewGroup, int, Object)
相应的ViewPager不会主动的添加Content View,也不会主动的移除Content View,所以亦需要在重写该方法的时候主动从ViewPager中移除该Content View。在ViewPager以下方法会调用destroyItem(ViewGroup, int, Object):
isViewFromObject(View,Object)
在使用ViewPager的时候,我曾试过未重写该方法,导致ViewPager运行不正常,所以这里我们看看isViewFromObject(View,Object)对ViewPager会有什么影响。isViewFromObject(View,Object)在ViewPager.infoForChild(View)中被调用到。所以我们分析下ViewPager.infoForChild(View)方法。
// 根据ViewPager Child View 去获取相应的Content View ItemInfo信息
ItemInfo infoForChild(View child) {
for (int i=0; i<mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
//判断该Child View与相应的Iteminfo中持有的对象是否一致
if (mAdapter.isViewFromObject(child, ii.object)) {
return ii;
}
}
return null;
}
至此谜底揭开,我们需要重写isViewFromObject(View,Object) 让ViewPager.infoForChild(View )能够正确判断某个Child View对应的ItemInfo信息。但是这里还是存在疑问,为什么这里ViewPager不写死该方法呢?
getCount()
这个方法不用多说了,在ViewPager中用的比较广。自行阅读源码。
至此,整个ViewPager与PagerAdapter也已经初步解析完毕了,你学到东西了吗?