自定义无限循环ViewPager分成了以下三篇文章进行讲解:
在前面两篇文章中,已经对ViewPager
的实现原理有了分析,相信大家对于ViewPager
的页面切换也有了一定的了解,接下来就是在ViewPager
的基础上对其进行改造,达到无限循环的目的。
在改造之前,我们先想想为什么当ViewPager滑动到第一页的时候,再向右滑动的时候,ViewPager无法滑动了?同理,当ViewPager滑动到最后一页的时候,再向左滑动的时候,ViewPager也无法滑动了?
在第一篇文章中,有分析过ViewPager的populate()
的方法,我们知道此方法主要作用是更新缓存列表以及计算缓存页面的偏移量。此方法中,在针对创建几个缓存页面的时候,存在着限制,这便是导致ViewPager在滑动到第一页和最后一页时,无法再滑动的原因。
//添加左边缓存页面
for (int pos = mCurItem - 1; pos >= 0; pos--) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
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));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
//添加右边缓存页面
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;
}
}
从上面的代码中,添加左边缓存页面,pos必须大于等于0,即当mCurItem=0
显示第一个页面时,它是没有左边的缓存页面的。而我们所希望的是,如果当mCurItem=0
,那么缓存列表中缓存页面的position的应该是[-1,0,1],而不是[0,1]。同理,添加右边缓存页面,pos必须小于N,即当mCurItem=N-1
最后一个页面的时,缓存列表应该是[N-2,N-1,N],而不是[N-2,N-1],其中N=mAdapter.getCount()。
所以要达到当显示第一个页面时,左边也存在缓存页面以及当显示最后一个页面,右边也存在缓存页面的目的,就必须放开添加缓存页面的for,而且position也不再代表页面在adapter数据中的位置, 如果要得到在adapter数据中的位置,需通过下面的公式得到。
Math.abs(N+ position%N) % N
如:
position=-1时,adapterIndex = N-1(最后一个页面position);
position=-2时,adapterIndex = N-2(倒数第二个页面position);
position=N时,adapterIndex = 0(第一个页面position);
position=N+1时,adapterIndex =1(第二个页面position);
populate()
所以populate()
方法改造如下
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;
}
if (mAdapter == null) {
sortChildDrawingOrder();
return;
}
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
if (getWindowToken() == null) {
return;
}
mAdapter.startUpdate(this);
final int pageLimit = mOffscreenPageLimit;
final int N = mAdapter.getCount();
//改动
//放开缓存页面的起始和结束的限制
int startPos = mCurItem - pageLimit;
int endPos = mCurItem + pageLimit;
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());
}
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;
}
}
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
if (curItem != null) {
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
final int clientWidth = getClientWidth();
final float leftWidthNeeded = clientWidth <=0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
//改动
//因为极端情况下,缓存的页面数为adpter数据的长度,所以需要遍历mExpectedAdapterCount-1次,
for (int pos = mCurItem -1; pos>=mCurItem-mExpectedAdapterCount; pos--) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, ii.position, ii.object);
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >=0 ? mItems.get(itemIndex) : null;
} 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 <= mCurItem+mExpectedAdapterCount ; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, ii.position, 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);
}
}
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
mAdapter.finishUpdate(this);
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(focusDirection)) {
break;
}
}
}
}
}
}
如果position不再代表页面在adapter数据中的位置,那么添加创建页面的addNewItem(int position, int index)
也需要进行改动,而且计算页面偏移量calculatePageOffsets()
方法中的mFirstOffset
和mLastOffset
赋值也需要做下改动。
addNewItem(int position, int index)
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
//改动,获取在adapter数据中的位置
int tempPos = Math.abs(mExpectedAdapterCount + position%mExpectedAdapterCount) % mExpectedAdapterCount;
ii.position = position;
//根据tempPos 创建页面
ii.object = mAdapter.instantiateItem(this, tempPos);
ii.widthFactor = mAdapter.getPageWidth(tempPos);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)
private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
final int N = mAdapter.getCount();
final int width = getClientWidth();
final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
// Fix up offsets for later layout.
if (oldCurInfo != null) {
final int oldCurPosition = oldCurInfo.position;
// Base offsets off of oldCurInfo.
if (oldCurPosition < curItem.position) {
int itemIndex = 0;
ItemInfo ii = null;
float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
for (int pos = oldCurPosition + 1;
pos <= curItem.position && itemIndex < mItems.size(); pos++) {
ii = mItems.get(itemIndex);
while (pos > ii.position && itemIndex < mItems.size() - 1) {
itemIndex++;
ii = mItems.get(itemIndex);
}
while (pos < ii.position) {
// We don't have an item populated for this,
// ask the adapter for an offset.
offset += mAdapter.getPageWidth(pos) + marginOffset;
pos++;
}
ii.offset = offset;
offset += ii.widthFactor + marginOffset;
}
} else if (oldCurPosition > curItem.position) {
int itemIndex = mItems.size() - 1;
ItemInfo ii = null;
float offset = oldCurInfo.offset;
for (int pos = oldCurPosition - 1;
pos >= curItem.position && itemIndex >= 0; pos--) {
ii = mItems.get(itemIndex);
while (pos < ii.position && itemIndex > 0) {
itemIndex--;
ii = mItems.get(itemIndex);
}
while (pos > ii.position) {
// We don't have an item populated for this,
// ask the adapter for an offset.
offset -= mAdapter.getPageWidth(pos) + marginOffset;
pos--;
}
offset -= ii.widthFactor + marginOffset;
ii.offset = offset;
}
}
}
// Base all offsets off of curItem.
final int itemCount = mItems.size();
float offset = curItem.offset;
int pos = curItem.position - 1;
//改动
mFirstOffset = -Float.MAX_VALUE;
mLastOffset = Float.MAX_VALUE;
// Previous pages
for (int i = curIndex - 1; i >= 0; i--, pos--) {
final ItemInfo ii = mItems.get(i);
while (pos > ii.position) {
offset -= mAdapter.getPageWidth(pos--) + marginOffset;
}
offset -= ii.widthFactor + marginOffset;
ii.offset = offset;
}
offset = curItem.offset + curItem.widthFactor + marginOffset;
pos = curItem.position + 1;
// Next pages
for (int i = curIndex + 1; i < itemCount; i++, pos++) {
final ItemInfo ii = mItems.get(i);
while (pos < ii.position) {
offset += mAdapter.getPageWidth(pos++) + marginOffset;
}
if (ii.position == N - 1) {
mLastOffset = offset + ii.widthFactor - 1;
}
ii.offset = offset;
offset += ii.widthFactor + marginOffset;
}
mNeedCalculatePageOffsets = false;
}
在第二篇文章中有分析到ViewPager的页面的滑动和切换存在两种方式,一种是拖拽通过调用performDrag()
方法得到需要滑动的距离,然后调用scrollTo()
方法完成页面的滑动,而在performDrag()
方法中会根据mFirstOffset
和mLastOffset
的值进行边界判断限制,但这两个值在calculatePageOffsets()
进行了最小和最大值的赋值更改,所以根据页面的偏移和滑动距离得到的最终滑动距离不会造成边界超出,因而此方式所涉及的方法不需要改造。
而第二种方式需要在手指抬起后,根据显示页面的偏移量得到目标页面,然后调用 setCurrentItemInternal()
滑动到目标页面。由于经过改造后,页面的偏移量存在负数,所以导致原来的determineTargetPage()
确定目标页面会存在问题,需要加以改造。
determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX)
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
int targetPage;
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
targetPage = velocity > 0 ? currentPage : currentPage + 1;
} else {
//改动
if (currentPage >= mCurItem) {
targetPage = pageOffset > 0.6f ? currentPage + 1 : currentPage;
} else {
targetPage = pageOffset > 0.4f ? mCurItem : currentPage;
}
}
if (mItems.size() > 0) {
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
// Only let the user target pages we have items for
targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
}
return targetPage;
}
setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity)
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
if (mAdapter == null || mAdapter.getCount() <= 0) {
setScrollingCacheEnabled(false);
return;
}
if (!always && mCurItem == item && mItems.size() != 0) {
setScrollingCacheEnabled(false);
return;
}
//改动,放开限制
// if (item < 0) {
// item = 0;
// } else if (item >= mAdapter.getCount()) {
// item = mAdapter.getCount() - 1;
// }
final int pageLimit = mOffscreenPageLimit;
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
// We are doing a jump by more than one page. To avoid
// glitches, we want to keep all current pages in the view
// until the scroll ends.
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
// We don't have any idea how big we are yet and shouldn't have any pages either.
// Just set things up and let the pending layout handle things.
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
总结
将Viewpager拷贝一份到自己的目录中去,将本文讲到需要改造的方法复制替换掉ViewPager原有的方法即可,这样就可以达到无限循环的目的了。不过值得一提的是,因为position不再代表页面在adapter数据中的位置,所以在各个接口回调中的position值,需要通过公式Math.abs(N+ position%N) % N
才能得到在adapter数据中真正的位置。
最后
关于改造ViewPager变为无限循环的第三部分所有内容就已经介绍完了,总的来说只要对ViewPager的相关原理有了一定的了解后,关于它的改造还是比较简单的。如果大家觉得本篇文章对各位有些帮助,希望能点个喜欢,谢谢!
网友评论