当给ViewPager的高度设置为wrap_content,并不会生效,原因在于onMeasure方法。onMeasure方法实用类测量宽高的,so每个View都会有自己的onMeasure方法。讲道理人家(的onMeasure方法)会先测量child的高度,最后再通过setMeasuredDimension方法测量并设置自己的宽高。
然而,ViewPager并没有这么做!上来就调用setMeasuredDimension方法设置自己的宽高,根本没有管它的child,简直就不是亲生的!
this.setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
从这行代码可以看出,VP将自己默认的size设为0,优先使用父布局传过来的widthMeasureSpec、heightMeasureSpec。结果就是如果不指定宽高,就使用match_parent。 只要VP符合它自己的parent的要求,VP就会快速的它的parent填充满,其他的一概不管。也许VP如此对待它的child的原因在于child可被动态的添加、删除,宽高并不确定!
要想解决这个问题,其实也很简单:只需要自定义ViewPager并重写onMeasure方法即可。在onMeasure方法中,先遍历自己所有的child并计算它们的高度,最后再调用父类的onMeasure方法,将得到的宽高传入:
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Fragment+ViewPager组合使用时,调用setOffscreenPageLimit(0)设置当前页面下左右两边缓存的item个数为0,无效。
public void setOffscreenPageLimit(int limit) {
if (limit < 1) {
Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1);
limit = 1;
}
if (limit != this.mOffscreenPageLimit) {
this.mOffscreenPageLimit = limit;
this.populate();
}
}
从以上代码中就能找到证据,当limit < 1时,limit = 1。也就是说即使设置缓存数为0,ViewPager始终还是会额外的去缓存下一个。对于VP+Fragment组合的使用场景而言,curItem是可见的,curItem+1无需展示,这样也会在请求网络上造成时间压力。
ViewPager + Adapter结合使用,VP是如何从Adapter处获取、销毁的数据呢?这就引出了一个神秘方法–populate() 。
populate() 是专门用来计算、处理缓存的,populate() 函数的生命周期是和Adapter的生命周期同步的。ViewPager能够顺利的对数据进行获取、销毁等操作,全部都依赖于populate() 。
void populate(int newCurrentItem) {
ViewPager.ItemInfo oldCurInfo = null;
if (this.mCurItem != newCurrentItem) {
oldCurInfo = this.infoForPosition(this.mCurItem);
this.mCurItem = newCurrentItem;
}
if (this.mAdapter == null) {
this.sortChildDrawingOrder();
} else if (this.mPopulatePending) {
this.sortChildDrawingOrder();
} else if (this.getWindowToken() != null) {
this.mAdapter.startUpdate(this);// --->更新
int pageLimit = this.mOffscreenPageLimit;
int startPos = Math.max(0, this.mCurItem - pageLimit);
int N = this.mAdapter.getCount();// --->获取Adapter的数据量
int endPos = Math.min(N - 1, this.mCurItem + pageLimit);
// 缓存空间[startPos,endPos] ---> [mCurItem-pageLimit,mCurItem+pageLimit]
if (N != this.mExpectedAdapterCount) {
String resName;
try {
resName = this.getResources().getResourceName(this.getId());
} catch (NotFoundException var17) {
resName = Integer.toHexString(this.getId());
}
throw new IllegalStateException("The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: " + this.mExpectedAdapterCount + ", found: " + N + " Pager id: " + resName + " Pager class: " + this.getClass() + " Problematic adapter: " + this.mAdapter.getClass());
} else {
int curIndex = true;
ViewPager.ItemInfo curItem = null;
int curIndex;
for(curIndex = 0; curIndex < this.mItems.size(); ++curIndex) {
ViewPager.ItemInfo ii = (ViewPager.ItemInfo)this.mItems.get(curIndex);
if (ii.position >= this.mCurItem) {
if (ii.position == this.mCurItem) {
curItem = ii;
}
break;
}
}
//curItem没找到,即没有缓存,就先加载一个item
if (curItem == null && N > 0) {
curItem = this.addNewItem(this.mCurItem, curIndex);
}
int itemIndex;
ViewPager.ItemInfo ii;
int i;
if (curItem != null) {
//先对左边的item进行缓存处理
float extraWidthLeft = 0.0F;
itemIndex = curIndex - 1;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
i = this.getClientWidth();
float leftWidthNeeded = i <= 0 ? 0.0F : 2.0F - curItem.widthFactor + (float)this.getPaddingLeft() / (float)i;
for(int pos = this.mCurItem - 1; pos >= 0; --pos) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
// 缓存区间之外的item,要destory掉
if (pos == ii.position && !ii.scrolling) {
this.mItems.remove(itemIndex);
this.mAdapter.destroyItem(this, pos, ii.object);// --->销毁item
--itemIndex;
--curIndex;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
--itemIndex;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
} else { //如果item没有出现过,就去addNewItem
ii = this.addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
++curIndex;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
}
//再对右边的item进行缓存处理
float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.0F) {
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
float rightWidthNeeded = i <= 0 ? 0.0F : (float)this.getPaddingRight() / (float)i + 2.0F;
for(int pos = this.mCurItem + 1; pos < N; ++pos) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
this.mItems.remove(itemIndex);
this.mAdapter.destroyItem(this, pos, ii.object);
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
++itemIndex;
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
} else {
ii = this.addNewItem(pos, itemIndex);
++itemIndex;
extraWidthRight += ii.widthFactor;
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
}
}
this.calculatePageOffsets(curItem, curIndex, oldCurInfo);
this.mAdapter.setPrimaryItem(this, this.mCurItem, curItem.object); // --->设置当前的item
}
this.mAdapter.finishUpdate(this);// --->完成更新
int childCount = this.getChildCount();
for(itemIndex = 0; itemIndex < childCount; ++itemIndex) {
View child = this.getChildAt(itemIndex);
ViewPager.LayoutParams lp = (ViewPager.LayoutParams)child.getLayoutParams();
lp.childIndex = itemIndex;
if (!lp.isDecor && lp.widthFactor == 0.0F) {
ViewPager.ItemInfo ii = this.infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
this.sortChildDrawingOrder();
if (this.hasFocus()) {
View currentFocused = this.findFocus();
ii = currentFocused != null ? this.infoForAnyChild(currentFocused) : null;
if (ii == null || ii.position != this.mCurItem) {
for(i = 0; i < this.getChildCount(); ++i) {
View child = this.getChildAt(i);
ii = this.infoForChild(child);
if (ii != null && ii.position == this.mCurItem && child.requestFocus(2)) {
break;
}
}
}
}
}
}
}
首先关注一点:
curItem = this.addNewItem(this.mCurItem, curIndex);
一旦没找到curItem,即没有缓存,就先加载一个item。curItem是通过addNewItem方法来确定的:
ViewPager.ItemInfo addNewItem(int position, int index) {
ViewPager.ItemInfo ii = new ViewPager.ItemInfo();
ii.position = position;
//ItemInfo中的object就是缓存的View
ii.object = this.mAdapter.instantiateItem(this, position);
ii.widthFactor = this.mAdapter.getPageWidth(position);
if (index >= 0 && index < this.mItems.size()) {
this.mItems.add(index, ii);
} else {
this.mItems.add(ii);
}
return ii;
}
而在addNewItem函数中,我们可以发现Adapter的身影:this.mAdapter.instantiateItem(this, position)。也就是说创建新的Item要通过Adapter,恰巧instantiateItem方法的作用就是用来构建新的View并return。
其实,只要仔细观察Adapter的方法:
public abstract int getCount();
public void startUpdate(@NonNull ViewGroup container)
public Object instantiateItem(@NonNull ViewGroup container, int position)
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object)
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object)
public void finishUpdate(@NonNull ViewGroup container)
......
在populate()方法中都能看到他们被调用的身影:
this.mAdapter.startUpdate(this);// --->更新
int N = this.mAdapter.getCount();// --->获取Adapter的数据量
this.mAdapter.destroyItem(this, pos, ii.object);// --->销毁item
this.mAdapter.setPrimaryItem(this, this.mCurItem, curItem.object); // --->设置当前的item
this.mAdapter.finishUpdate(this);// --->完成更新
......
这意味着,populate()函数贯穿了整个Adapter的各个函数的调用过程,等同于populate()方法的生命周期和Adapter已经融合在一起了。换句话说,Adapter的所有动作都是由populate()方法操控的,即每个Item的管理实际上都是由populate()方法控制的。可以得出结论:VP的缓存的幕后黑手是populate()。就好比是宇智波带土一直在用写轮眼控制三尾人柱力矢仓,对鬼鲛下达命令。
说道缓存,仔细看populate()方法中的startPos和endPos。VP的缓存空间就是[startPos,endPos] —> [mCurItem-pageLimit,mCurItem+pageLimit]。这里的pageLimit就是我们在Java代码中调用setOffscreenPageLimit(n)时传入的值。这样计算来,VP缓存的是[当前Item-pageLimit,当前Item+pageLimit]。
在完成更新之前,要对左右两边的缓存进行处理:
if (curItem != null) {
//先对左边的item进行缓存处理
float extraWidthLeft = 0.0F;
itemIndex = curIndex - 1;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
i = this.getClientWidth();
float leftWidthNeeded = i <= 0 ? 0.0F : 2.0F - curItem.widthFactor + (float)this.getPaddingLeft() / (float)i;
for(int pos = this.mCurItem - 1; pos >= 0; --pos) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
// 缓存区间之外的item,要destory掉
if (pos == ii.position && !ii.scrolling) {
this.mItems.remove(itemIndex);
this.mAdapter.destroyItem(this, pos, ii.object);// --->销毁item
--itemIndex;
--curIndex;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
--itemIndex;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
} else { //如果item没有出现过,就去addNewItem
ii = this.addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
++curIndex;
ii = itemIndex >= 0 ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
}
//再对右边的item进行缓存处理
float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.0F) {
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
float rightWidthNeeded = i <= 0 ? 0.0F : (float)this.getPaddingRight() / (float)i + 2.0F;
for(int pos = this.mCurItem + 1; pos < N; ++pos) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
this.mItems.remove(itemIndex);
this.mAdapter.destroyItem(this, pos, ii.object);
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
++itemIndex;
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
} else {
ii = this.addNewItem(pos, itemIndex);
++itemIndex;
extraWidthRight += ii.widthFactor;
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
}
}
this.calculatePageOffsets(curItem, curIndex, oldCurInfo);
this.mAdapter.setPrimaryItem(this, this.mCurItem, curItem.object); // --->设置当前的item
}
处理顺序为:先左右后。缓存区间之外的Item要及时destroyItem销毁掉。当然,如果Item没有出现,就先去addNewItem构建出一个Item来,并保存到mItems中。这个mItems是专门用来缓存的ArrayList:
//mItems是专门用来缓存的ArrayList
private final ArrayList<ViewPager.ItemInfo> mItems = new ArrayList();
泛型为ItemInfo:
static class ItemInfo {
Object object; // --->缓存的就是View
int position;
boolean scrolling;
float widthFactor;
float offset;
ItemInfo() {
}
}
这个ItemInfo类中有众多信息,其中的 Object object 就是专门用来缓存View的。因此,ViewPager才会去缓存界面。仔细看addNewItem方法中的一行代码:
//ItemInfo中的object就是缓存的View
ii.object = this.mAdapter.instantiateItem(this, position);
想想看Adapter调用instantiateItem是干什么的?同时又返回了什么?
这样一来就解释的通了:当需要构建View的时候,addNewItem函数会通过调用Adapter的instantiateItem方法来创建,return的View会被保存到专门用来缓存的ItemInfo的object中。
如果对方是Fragment,那么观察FragmentPagerAdapter:
@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (this.mCurTransaction == null) {
this.mCurTransaction = this.mFragmentManager.beginTransaction();
}
long itemId = this.getItemId(position);
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
this.mCurTransaction.attach(fragment);
} else {
fragment = this.getItem(position);
this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
}
if (fragment != this.mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
新建并返回、保存的就是fragment了。至此,ViewPager的缓存原理就十分清晰了。
缓存有好处,也有不友好的地方。对Fragment而言,如果不进行懒加载处理,那是非常糟糕的。一般对Fragment进行懒加载处理,不能单一的根据setUserVisibleHint或者onResume来判断。Fragment的界面可见状态分为3种:第一次可见、可见、不可见。这两个函数都不能很好的对其进行精准区分。
在这方面,我们需要关心Fragment的几个生命周期函数:onCreateView、onActivityCreated、onResume、onPause、onDestroyView。
onCreateView,最先想到的就是解析xml布局(这也是最根本的),其次就是初始化控件。
onActivityCreated,表示Activity创建完毕。
onResume、onPause和onDestoryView无需多言,常规认识。
关于setUserVisibleHint,它与Fragment的生命周期无关,仅仅是为了表达页面是否对用户可见。还记得FragmentPagerAdapter吗?这个适配器它内部的setPrimaryItem方法是用来设置当前Item是否可见的,方法内调用的就是setUserVisibleHint(true)方法:
关于setUserVisibleHint方法修改Fragment可见性,调用场景如下:
setUserVisibleHint(false)表示通知Fragment不可见,即终止一切网络请求!但是如果从未创建过就要怎样怎样,那就会报空指针异常。所以需要一个标志位来判断界面是否已经创建。只有当页面被创建出来了,才会去分发可见性。否则xml都还没加载,分发个球?不报空指针才怪呢。
所以,在setUserVisibleHint方法的处理上,我们根本不用考虑isViewCreated==false的情况。当isViewCreated为true时,我们再根据传进来的可见状态与当前页面可见状态,分发可见性。
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//只考虑xml加载出来的情况
if (isViewCreated) {
//根据传进来的状态 和 当前页面的可见性,判断分发是否可见
if (isVisibleToUser && !currentVisibleState ) {
dispatchUserVisibleHint(true);
} else if (!isVisibleToUser && currentVisibleState) {
dispatchUserVisibleHint(false);
}
}
}
我们通过dispatchUserVisibleHint方法来统一处理用户可见信息的分发,在分发方法中,再区分是否是第一次可见。因为只有当第一次可见时,才会去主动做一些事的。否则其他情况的可见性,就通知用户可见状态,履行onResume()的职责。
当然,这里还需要用到currentVisibleState表示当前页面可见性,借助mIsFirstVisible 标记区分是否是第一次可见。
private void dispatchUserVisibleHint(boolean isVisible) {
//为了代码严谨
if (currentVisibleState == isVisible) {
return;
}
currentVisibleState = isVisible;
if (isVisible) {
if (mIsFirstVisible) {
mIsFirstVisible = false;
onFragmentFirstVisible();
}
onFragmentResume();
} else {
onFragmentPause();
}
}
另外,当FragmentTransaction来控制fragment的hide和show时,就会调用onHiddenChanged方法。此时,Fragment的生命周期函数不会再执行,任何生命周期都不会再走了(具体原因点击查看)。在这种情况下,数据就不能根据生命周期函数来判断刷新,此时可以依赖onHiddenChanged方法来继续判断处理:
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden) {
dispatchUserVisibleHint(false);
} else {
dispatchUserVisibleHint(true);
}
}
剩下的工作,就是处理边边角角了。在onResume和onPause中根据是否第一次可见、当前页面可见性、getUserVisibleHint等对是否可见、是否第一次可见进行区分,并以此来分发可见性。
当然,如果考虑Fragment内嵌套Fragment,这里还得再多处理一层,那就是对内层Fragment分发可见性。代码不一样,但是性质差不多。首先要判断其Parent是否可见:
private boolean isParentInvisible() {
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof LazyFragment) {
LazyFragment fragment = (LazyFragment)parentFragment;
return !fragment.isSupportVisible();
}
return false;
}
然后对统一分发进行小改,综合考虑Parent之后,再对Child的可见性向下分发:
private void dispatchChildVisibleState(boolean visible) {
FragmentManager fragmentManager = getChildFragmentManager();
List<Fragment> fragments = fragmentManager.getFragments();
if (fragments != null) {
for (Fragment fragment: fragments) {
if (fragment instanceof LazyFragment &&
!fragment.isHidden() &&
fragment.getUserVisibleHint()) {
((LazyFragment)fragment).dispatchUserVisibleHint(visible);
}
}
}
}