最近想撸一个垂直方向的VerticalViewPager,如果想要把它做到屌,那自然是要参考下现有我们的ViewPager实现。
该篇从ViewPager的measure与layout着手,解读ViewPager如何来实现自身已经childView的测量与布局。
onMeasure():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//直接设置ViewPager自身的大小,这里可以看出ViewPager设置wrap_content时不起作用
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
//mGutterSize是在事件分发时用来判断是否拖拽的一个阈值,影响是否拦截事件。
final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// 设置完宽高后计算出ViewPager实际可用显示的宽高
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
/**
* 以下为测量DecorView的过程
* 这里的 DecorView是ViewPager内部定义的注解
* ViewPager还可以通过xml布局添加item
* <ViewPager>
* <DecorView/>
* </ViewPager>
* 所以DecorView是用来表示在xml布局中添加的子view,在xml布局中添加子view时需要添加@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) {
//这里省略DecorView的具体测量过程
//…………
//ViewPager实际可用显示的宽高减去DecorView已占用的宽高
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
//计算去除decorView占位后的宽高测量模式(用于adapter中添加的ChildView)
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
/**
* populate()方法是ViewPager的一个核心方法了,它里面有矫正当前ViewPager上需要加载的ItemInfo,
* 下面会详细分析【ItemInfo】^①和【populate()】^③
*/
mInLayout = true;
populate();
mInLayout = false;
/* *
* 测量非DecorView的子View(通过Adapter添加的ChildView)
* 上面通过populate()已经矫正过需要加载的Item,
* 所以下面的循环的getChildCount并不会等于DecorView的数量+Adapter.getCount()
* (实际情况是: getChildCount() = DecorView数量+mItems.size(),
* mItem中存放当前ViewPager需要加载的ItemInfo,populate()后会得到最新的mItems)
*/
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);
}
//这里的LayoutParams为【ViewPager.LayoutParams】^②,下面会详细分析
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//这个lp == null严重怀疑是个笔误,应该是lp != null
//同时插一点lp.widthFactor,可以看到该值会直接决定ViewPager中ChildView的显示宽度。
//可以通过复写Adapter.getPageWidth()方法返回值改变该值。
if (lp == null || !lp.isDecor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
总结一下onMeasure()的整体流程:
1、设置ViewPager自身的大小;
2、算出可用大小后为每个子DecorView测量;
3、通过populate()方法调整ViewPager当前需要加载的itemInfo
4、为当前ViewPager加载的所有非DecorView测量。
接下来分析onMeasure预留的3个重要对象或方法:
①:ItemInfo
ItemInfo用来保存当前ViewPager需要加载的子View的相关信息。
static class ItemInfo {
Object object;//childView,该值为Adapter.instantiateItem()的返回值
int position;//childView在viewPager中的位置
boolean scrolling;//是否在滚动
float widthFactor;//宽度的占比,可以通过复写Adapter.getPageWidth()方法返回值改变该值,默认返回1。
float offset;//页面的偏移,用来决定layout时childView的位置
}
②:ViewPager.LayoutParams
ViewPager.LayoutParams中主要关心以下几个重要字段。
//是否为DecorView
public boolean isDecor;
// childView宽度与ViewPager宽度的比值(实际是可以超过1的)
float widthFactor = 0.f;
// 与Adapter中position相对应,DecorView不用关心该字段
int position;
// childView在mItems中的位置
int childIndex;
③:populate()
populate()方法较长,逻辑代码较多,不过耐心点阅读难度也不高。
void populate(int newCurrentItem) {
/**
*省略部分代码
*……
*/
/**
* 这里mOffscreenPageLimit决定缓存数量,
* 可以通过setOffscreenPageLimit()方法来该表该值
* mItems最多只会缓存1(mCurItem)+2*mOffscreenPageLimit个item,
* mCurItem左右不足mOffscreenPageLimit个item时则达不到最大数量。
* 以下starPos与endPos分别表示mItems中缓存的起始与结束position
*/
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);
if (N != mExpectedAdapterCount) {
String resName;
try {
/**
* 知识点,系统提供了根据id获取id名字的方法(之前傻傻反射去获取)
*/
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());
}
/**
* 在mItems中找到当前的ItemInfo
* 这里需要区分mCurItem和curIndex
* mCurItem表示当前item对应所有item的position
* curIndex表示当前itemInfo对应items中的位置
*/
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
/**
* 如果当前未找到对应的ItemInfo,
* addNewItem()方法会调用Adapter.instantiateItem()方法往ViewPager中添加对应mCurItem的childView,
* 同时根据Adapter.instantiateItem()的返回值创建对应的ItemInfo并添加到mItems
*/
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 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;
int itemIndex = curIndex - 1;
//获取当前ItemInfo的左边的ItemInfo li,下面循环从这里开始
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//获取到viewPager真实可用宽度
final int clientWidth = getClientWidth();
/**
* leftWidthNeeded表示左边打到回收条件的宽度比例的阈值,
* 以下处理三种场景
* 场景1:当左边ItemInfo的extraWidthLeft超过leftWidthNeeded,
* 且对应的位置不在上面计算的startPos-endPos范围内时将从mItems中移除(如果存在)
* 同时调用Adapter.destroyItem()通知Adapter回收itemInfo对应的view
* 场景2:在显示范围内且存在,继续下一次循环
* 场景3:在显示范围内且不存在,则通过addNewItem()方法调用Adapter.instantiateItem()方法
* 往ViewPager中添加对应mCurItem的childView,
* 同时根据Adapter.instantiateItem()的返回值创建对应的ItemInfo并添加到mItems
*/
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
/**
* 遍历mCurItem左边的ItemInfo
*/
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//场景1
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
//移除缓存时回调 mAdapter.destroyItem
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
//场景2
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//场景3
} else {
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;
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;
}
}
}
/**
* 这里在整理好缓存items后,计算每个items里的{@link ItemInfo#offset}偏移量
*/
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
/**
* 这里是知识点,adapter会讲当前的{@link ItemInfo#object}通过setPrimaryItem回调给adapter,
* 这个curItem.object就是Adapter中instantiateItem(ViewGroup container, int position)方法的返回值,一般都会返回我们添加的chilView
* 所以在我们需要获取ViewPager当前的childView时,我们可以在Adapter中可以复写setPrimaryItem方法将curItem.object保存起来,
*/
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
mAdapter.finishUpdate(this);
/**
* 将以上更新的ItemInfo中的内容更新到对应childView的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();
//焦点传递
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;
}
}
}
}
}
}
populate()代码稍微有些长,但是核心逻辑也不难理解,它实现了ViewPager的添加childView和删除childView的功能,大致流程为:
1、根据mOffscreenPageLimit计算mItems将要储存ViewPager中startPos~endPos对应的ItemInfo;
2、从mItems中获取当前mCurItem对应的ItemInfo,若没有则通过addNewItem()方法调用Adapter.instantiateItem()为ViewPager添加mCurItem位置的childView。同时创建该位置对应的ItemInfo并添加到mItems中;
3、循环遍历当前item左边的所有item,若不在leftWidthNeeded与startPos-endPos决定范围内,则从mItems中删除,同时调用Adapter.destroyItem()方法来通知Adapter回收itemInfo对应的view。若在该范围内且不存在时则通过addNewItem()方法调用Adapter.instantiateItem()方法 往ViewPager中添加对应mCurItem的childView,同时根据Adapter.instantiateItem()的返回值创建对应的ItemInfo并添加到mItems;
4、循环遍历当前item右边的所有item,逻辑与左循环基本一致;
5、通过以上流程整理好需要加载的mItems后,计算mItems里的每个ItemInfo的偏移量,用来决定layout时的位置
6、最后将以上更新的ItemInfo中的内容更新到对应childView的LayoutParams中
populate()方法决定了ViewPager添加childView以及删除childView,在整个ViewPager源码中多个方法中调用,有:onMeasure()、setAdapter()、onInterceptTouchEvent()、setAdapter()、setPageTransformer()、setOffscreenPageLimit()、smoothScrollTo()等多个涉及到页面发生变化的方法中调用,由此可见它在ViewPager中时非常重要的一个方法,所以好好理解它还是非常有必要的。
onLayout():
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//……略略略……
// 首先对子DecorView进行layout
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) {
//……略略略……
childLeft += scrollX;
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
decorCount++;
}
}
}
//对Adapter中添加的childView进行layout
final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
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) {
//……略略略……
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
mTopPageBounds = paddingTop;
mBottomPageBounds = height - paddingBottom;
mDecorChildCount = decorCount;
//首次layout需要将viewPager滚动到对应的mCurItem位置
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false;
}
可以看到onLayout()方法还是比较简单的,通过循环DecorView与非DecorView(Adapter中添加的childView)结合他们对应的LayoutParams,对所有childView进行layout。
到这里ViewPager的onMeasure与onLayout就分析完了,重要的事情最后在提醒一次整个流程的重点就在populate()方法的流程,populate()也是整个ViewPager的一个核心方法,还是要耐心将它读完的。
到这里ViewPager的onMeasure、onLayout、populate就已经分析完了,下一篇《ViewPager源码解析(二):setAdapter,notifyDataSetChanged》
会进一步分析ViewPager的数据绑定与刷新。