看一点记录一点,虽然网上很多大佬都有记录,但还是想自己学完记录下
ViewPager继承自ViewGroup,先从onMeasure、onLayout、onDraw开始看起
onMeasure
主要做了这几件事:
- 对整个ViewPager的大小进行设置。设置的大小为父类传递过来的大小,也就是剩余的空间,对这个不理解的可以看博主另一篇文章Android自定义时间轴
- 测量DecorView,并对其设置大小。使用ViewPager.DecorView注释的视图被视为ViewPager“装饰”的一部分。具体可以看博主另一篇文章PagerTitleStrip使用
- 测量Adapter的所有View,也就是每个Item,并设置其大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置ViewPager的大小,设置的大小为父类传递的剩余空间
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// Children are just made to fill our space.
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
/*
* Make sure all children have been properly measured. Decor views first.
* Right now we cheat and make this less complicated by assuming decor
* views won't intersect. We will pin to edges based on gravity.
*/
//开始设置DecorView的大小
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();
if (lp != null && lp.isDecor) {
//判断是不是设置了Gravity.LEFT || hgrav == Gravity.RIGHT
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
//判断是不是设置了Gravity.TOP || vgrav == Gravity.BOTTOM
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int widthMode = MeasureSpec.AT_MOST;
int heightMode = MeasureSpec.AT_MOST;
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode = MeasureSpec.EXACTLY;
}
int widthSize = childWidthSize;
int heightSize = childHeightSize;
if (lp.width != LayoutParams.WRAP_CONTENT) {
widthMode = MeasureSpec.EXACTLY;
if (lp.width != LayoutParams.MATCH_PARENT) {
widthSize = lp.width;
}
}
if (lp.height != LayoutParams.WRAP_CONTENT) {
heightMode = MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.MATCH_PARENT) {
heightSize = lp.height;
}
}
//对DecorView进行设置长和宽
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
child.measure(widthSpec, heightSpec);
if (consumeVertical) {
//ViewPager剩余的高(也就是每个Item的高)是减掉DecorView的高
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
//ViewPager剩余的宽(也就是每个Item的宽)是减掉DecorView的宽
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
//ViewPager宽度剩余的MeasureSpec,没用上
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
//ViewPager高度剩余的MeasureSpec
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
populate();
mInLayout = false;
// Page views next.
//开始设置每个Item的宽高
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
if (DEBUG) {
Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
//生成每个Item的宽的MeasureSpec
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
//对每个Item的宽高进行设置
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
populate
主要做了这几件事:
- 根据传入的当前界面更新mItems数组,该数组存放的是对应的ItemInfo对象
- 如果左面超出了缓存限制,则删除左边,如果左边需要缓存却没有缓存,则在左边创建对应的ItemInfo
- 如果右面超出了缓存限制,则删除右边,如果右边需要缓存却没有缓存,则在右边创建对应的ItemInfo
- 如果当前ViewPager可获得焦点,把焦点传递给子View
//newCurrentItem表示是当前显示的界面(也就是传入的Position)
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
//如果传入进来的Item不等于当前Item
if (mCurItem != newCurrentItem) {
//得到之前的ItemInfo
oldCurInfo = infoForPosition(mCurItem);
//更新Position
mCurItem = newCurrentItem;
}
//当mAdapter为null时,如果调用了setPageTransformer,传递了true选项,就会对子View进行排序,优先绘制Decor View
if (mAdapter == null) {
sortChildDrawingOrder();
return;
}
/**
private void sortChildDrawingOrder() {
if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
if (mDrawingOrderedChildren == null) {
mDrawingOrderedChildren = new ArrayList<View>();
} else {
mDrawingOrderedChildren.clear();
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
mDrawingOrderedChildren.add(child);
}
Collections.sort(mDrawingOrderedChildren, sPositionComparator);
}
static class ViewPositionComparator implements Comparator<View> {
@Override
public int compare(View lhs, View rhs) {
final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
if (llp.isDecor != rlp.isDecor) {
return llp.isDecor ? 1 : -1;
}
return llp.position - rlp.position;
}
}
}**/
// Bail now if we are waiting to populate. This is to hold off
// on creating views from the time the user releases their finger to
// fling to a new position until we have finished the scroll to
// that position, avoiding glitches from happening at that point.
//TODO 待研究
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
// Also, don't populate until we are attached to a window. This is to
// avoid trying to populate before we have restored our view hierarchy
// state and conflicting with what is restored.
//在ViewPager没有attached之前,不要进行populate.
if (getWindowToken() == null) {
return;
}
//调用Adapter的startUpdate方法
mAdapter.startUpdate(this);
//mOffscreenPageLimit就是我们通过setOffscreenPageLimit设置的参数,默认为1
final int pageLimit = mOffscreenPageLimit;
//这个startPos是缓存页面的起始页,如果mCurItem是1,mCurItem - pageLimit为0。如果mCurItem是2,mCurItem - pageLimit为1
final int startPos = Math.max(0, mCurItem - pageLimit);
//得到ViewPager的所有Item
final int N = mAdapter.getCount();
//这个endPos是缓存页面的终止页,如果mCurItem是1,mCurItem + pageLimit为2,如果mCurItem是2,mCurItem + pageLimit为3
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
//从抛出异常的情况来看,应该是用户改变了PagerAdapter的内容,但是没用调用notifyDataSetChanged方法,导致数量不一致了
if (N != mExpectedAdapterCount) {
String resName;
try {
resName = getResources().getResourceName(getId());
} catch (Resources.NotFoundException e) {
resName = Integer.toHexString(getId());
}
throw new IllegalStateException("The application's PagerAdapter changed the adapter's"
+ " contents without calling PagerAdapter#notifyDataSetChanged!"
+ " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N
+ " Pager id: " + resName
+ " Pager class: " + getClass()
+ " Problematic adapter: " + mAdapter.getClass());
}
// Locate the currently focused item or add it if needed.
//存放mItems数组中的位置
int curIndex = -1;
//mCurItem在mItems里面,就返回对应的ItemInfo,否则,就创建一个ItemInfo放到mItems里面,并赋值给curItem
ItemInfo curItem = null;
//遍历mItems存放的ItemInfo对象,找到当前获得焦点的页面,更新ItemInfo,然后跳出
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
/**
static class ItemInfo {
//调用instantiateItem生成的View
Object object;
//当前的position
int position;
boolean scrolling;
//在Adapter的getPageWidth方法返回,用于显示当前页面
//的比例,最后将会在onLayout方法中通过
//childWidth * lp.widthFactor测量当前View的宽度
float widthFactor;
//这个参数暂时也没咋懂,反正是通过widthFactor + marginOffset = width > 0 ? (float) mPageMargin / width : 0;
float offset;
}
**/
//如果curItem为空,说明,mItems没有任何东西,或者说当前界面ItemInfo没有保存到mItems里面去
if (curItem == null && N > 0) {
//mCurItem是position的位置
//curIndex是mCurItem所对应的ItemInfo在mItems中的位置
curItem = addNewItem(mCurItem, curIndex);
/**
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
**/
}
// Fill 3x the available width or up to the number of offscreen
// pages requested to either side, whichever is larger.
// If we have no current item we have no work to do.
if (curItem != null) {
float extraWidthLeft = 0.f;
//mCurItem所对应的ItemInfo在mItems中的上一个位置
int itemIndex = curIndex - 1;
//获得焦点View的上一个ItemInfo,没有就为null
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//得到ViewPager的宽度可用大小
final int clientWidth = getClientWidth();
//从下文看,缓存的原理就是缓存当前页面和左右两边的页面(左右两边根据pageLimit计算出来的)
//假如当前是2,那么pos是1,1是应该缓存的界面,extraWidthLeft += ii.widthFactor extraWidthLeft为1
//pos--
//这时如果mItems左面还有view,那么应该是需要移除的view
//extraWidthLeft >= leftWidthNeeded && pos < startPos成立
//进行移除
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem .widthFactor + (float) getPaddingLeft() / (float) clientWidth;
//从当前界面左边开始,不包括自身
//此方法主要是删除左边不在缓存范围内和如果左边没有缓存,在左边创建缓存
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//pos < startPos找到需要移出去的ItemInfo
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
//如果左边没有界面,跳出循环。
if (ii == null) {
break;
}
//
if (pos == ii.position && !ii.scrolling) {
//将需要移除的itemInfo从mItems中移除
mItems.remove(itemIndex);
//调用Adapter的destroyItem方法,
//pos是position
//ii.object是移除的View
//如果Adapter不重写会报异常throw new UnsupportedOperationException("Required method destroyItem was not overridden");
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
//mItems里面位置变化
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
//当前位置是需要缓存的界面,将左边宽度加上上一个页面的宽度
else if (ii != null && pos == ii.position) {
//extraWidthLeft += ii.widthFactor
extraWidthLeft += ii.widthFactor;
//itemIndex--
itemIndex--;
//获取mItems上一个的上一个itemInfo
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
//这个位置需要缓存,但是却没有对应ItemInfo,
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
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;
//此方法主要是删除右边不在缓存范围内和如果右边没有缓存,在右边创建缓存
//假如当前是1,那么pos是2,2是应该缓存的界面,extraWidthLeft += ii.widthFactor extraWidthLeft为2
//这时如果mItems右面还有view,那么应该是需要移除的view
//extraWidthRight >= rightWidthNeeded && pos > endPos成立
//进行移除
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
//计算偏移量
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
if (DEBUG) {
Log.i(TAG, "Current page list:");
for (int i = 0; i < mItems.size(); i++) {
Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
}
}
//回调当前setPrimaryItem方法,参数为当前显示的界面
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
//回调,更新结束
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
//将mItems里面ItemInfo信息更新到LayoutParams
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn't have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
//重新进行排序
sortChildDrawingOrder();
//如果当前ViewPager是具有焦点的,将焦点分发给子View
if (hasFocus()) {
View currentFocused = findFocus();
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
if (ii == null || ii.position != mCurItem) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ii = infoForChild(child);
if (ii != null && ii.position == mCurItem) {
if (child.requestFocus(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
onLayout
主要做了这几件事:
- 设置Decor View的位置
- 设置普通View的位置
- 将页面移动到第一个Item位置
@Override
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;
// First pass - decor views. We need to do this in two passes so that
// we have the proper offsets for non-decor views later.
//设置Decor View的位置
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) {
//判断DecorView是不是设置了Gravity.LEFT || hgrav == Gravity.RIGHT
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
//判断DecorView是不是设置了Gravity.TOP || vgrav == Gravity.BOTTOM
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
//因为作者的代码是设置了Gravity.TOP
switch (hgrav) {
default:
//进入default将childLeft设置为paddingLeft
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
paddingLeft += child.getMeasuredWidth();
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;
}
switch (vgrav) {
default:
childTop = paddingTop;
break;
case Gravity.TOP:
//childTop设置为paddingTop
//paddingTop为DecorView的高
childTop = paddingTop;
paddingTop += child.getMeasuredHeight();
break;
case Gravity.CENTER_VERTICAL:
childTop = Math.max((height - child.getMeasuredHeight()) / 2,
paddingTop);
break;
case Gravity.BOTTOM:
childTop = height - paddingBottom - child.getMeasuredHeight();
paddingBottom += child.getMeasuredHeight();
break;
}
childLeft += scrollX;
//确定DecorView的位置
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
//将改DecorView的计数器加1
decorCount++;
}
}
}
final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
//设置非Decor View的位置
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
//如果子 View不是Decor View 并且子 View保存在mItems中
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
//这个loff是计算的页面的左面位置
//第一个View的ii.offset为0,loff为0
//第二个View的ii.offset为1.0,loff为childWidth * ii.offset
//效果图如下所示
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
//测量
if (lp.needsMeasure) {
// This was added during layout and needs measurement.
// Do it now that we know what we're working with.
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);
}
if (DEBUG) {
Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+ "x" + child.getMeasuredHeight());
}
//设置普通View的布局
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
//mTopPageBounds全局变量,是普通View的Top(刨除掉Decor View)
mTopPageBounds = paddingTop;
//mBottomPageBounds全局变量,是普通View的Bottom(刨除掉Decor View)
mBottomPageBounds = height - paddingBottom;
//Decor View的个数
mDecorChildCount = decorCount;
//第一次布局,滑动到第一个Item
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
//将第一次布局标志置为false
mFirstLayout = false;
}