ViewPager是现在比较常用的图片轮播容器,本人在项目中做循环轮播时,踩了不少坑,特开一贴记录。
这是个老生常谈的问题了,网上的解决方案也一堆,这里主要记录一下问题产生的原因。
每次调用notifyDataSetChanged()
时,都会触发getItemPosition(Object object)
。该方法会遍历viewpager
的所有item
,为每个item
返回一个状态值(POSITION_NONE/POSITION_UNCHANGED)
。
如果item
的位置如果没有发生变化,则返回POSITION_UNCHANGED
。如果返回了POSITION_NONE
,表示该位置的item
已经不存在了。默认的实现是假设item
的位置永远不会发生变化,而返回POSITION_UNCHANGED
。 所以如果我们不重写getItemPosition(Object object)
,就无法看到刷新效果。
对于这两种状态:
如果是POSITION_NONE
,那么该item
会被destroyItem(…)
方法remove掉,然后重新加载;
如果是POSITION_UNCHANGED
,就不会重新加载。
所以我们解决的方案也明了了:初始化item
时,把item
的position
作为tag
与它绑定,然后重写getItemPosition
,遇到当前位置上的item就把它的状态置为POSITION_NONE
。
@Override
public Object instantiateItem(View collection, int position) {
final int count = getSize();
if (count > 0 && position >= count) {
position = position % count;
}
ViewPager viewPager = ((ViewPager) collection);
View childView = getView(cachedView, mList.get(position), position);
viewPager.addView(childView);
childView.setTag(KEY_TAG_CURRENT_POS, position);
return childView;
}
@Override
public int getItemPosition(Object object) {
View view = (View) object;
Object o = view.getTag(KEY_TAG_CURRENT_POS);
if (o != null) {
final int curPos = (Integer) o;
//只刷新当前页面
if (mCurrentPosition != curPos) {
return POSITION_UNCHANGED;
}
}
return POSITION_NONE;
}
/**
* 外部调用,当viewPager滑动页面时,保存当前位置
**/
public void setCurrentPosition(int mCurrentPosition) {
this.mCurrentPosition = mCurrentPosition;
}
ViewPager被设计成预加载的形式,默认会维护三个页面:当前页面,下一页面和上一页面。每向前滑动一页,会destroy掉最后面的一项,然后预加载前面一项,如图:
因此如果元素个数大于3时,不会有什么问题,而一旦元素小于等于3,就会出现一系列问题。
我在项目中设计循环轮播时,想把所有的item
都缓存下来,省的每次都重新new
新对象,况且我的item
里还有图,不加缓存,时间久了很容易OOM
,代码如下:
@Override
public Object instantiateItem(View collection, int position) {
final int count = getSize();
if (count > 0 && position >= count) {
position = position % count;
}
ViewPager viewPager = ((ViewPager) collection);
View cachedView = null;
if (mCachedViewList != null) {
cachedView = mCachedViewList.get(position);
}
View childView = getView(cachedView, mList.get(position), position);
if (cachedView == null) {
mCachedViewList.put(position, childView);
}
viewPager.addView(childView);
childView.setTag(KEY_TAG_CURRENT_POS, position);
return childView;
}
当只有三个元素,向正向滑动再反方向滑动时,会报错:
java.lang.IllegalArgumentException: parameter must be a descendant of this view
错在两次在同一位置调用instantiateItem
,重复添加了子View
。
viewPager.addView(childView);
下面分析一下:
如果定位到ViewPager的源码,直接看populate(int newCurrentItem),这里就不贴源码了。
ViewPager在滑动过程中都是从左到右,根据位置分析当前是销毁(调用destroyItem),还是使用缓存,还是重新初始化item(调用instantiateItem)。
假如当时在位置2,正向滑动一次,到了位置3,此时先destory位置1,预加载位置4,由于是循环滑动,修正后会重新加载位置1;
接着反向滑动,到了位置2,此时需要先预加载位置1,再次调用了instantiateItem,此时就会再次添加item 1,结果就报错了…
除此之外,如果页面数量少于3,比如只有两个页面,会出现,滑着滑着就一堆空白页面,这也是ViewPager子数量太少引起的。
怎么解决呢?
核心问题就在于,子元素太少,所以网上的通用方案是当子元素小于3时,再次添加一遍子元素,让子元素数量强制大于3。
渲染indicator
时,要用真实数量,所以还要在ViewPagerAdapter
中保存真实数量。