一、项目地址
只需要在原来的开发人员写的界面的基础上添加二个界面就可以了,就是原来的count数量上变为count+2
大神Jake Wharton也是用的这种方式:
You can see a sample usage on ViewPagerIndicator fork (by Jake Wharton)
or on PagerSlidingTabStrip fork (by Andreas Stütz)
二、使用
直接替换< android.support.v4.view.ViewPager>为< com.xs.view.LoopViewPager>即可
然后其它的用法和官方的ViewPager的用法一样
instantiateItem() 方法父组件的处理:通常我们会直接addView,但这里如果直接这样写,则会抛出IllegalStateException。假设一共有三个view,则当用户滑到第四个的时候就会触发这个异常,原因是我们试图把一个有父组件的View添加到另一个组件
destroyItem() 方法:由于我们在instantiateItem()方法中已经处理了remove的逻辑,因此这里并不需要处理。
- 实际上,实验表明这里如果加上了remove的调用,则会出现ViewPager的内容为空的情况。具体原因可以参考上面的ViewPager的原理,比如说当前是最后一个位置4,向右滑动肯定要到0位置的,但是0位置已经被销毁了,所以View就不存在了
public class MyViewPagerAdapter extends PagerAdapter{
private List<View> mListViews;
public MyViewPagerAdapter(List<View> mListViews) {
this.mListViews = mListViews;//构造方法,参数是我们的页卡,这样比较方便。
}
//直接继承PagerAdapter,至少必须重写下面的四个方法,否则会报错
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
//container.removeView(mListViews.get(position));//删除页卡
}
@Override
public Object instantiateItem(ViewGroup container, int position){
//这个方法用来实例化页卡
if(mListViews.get(position).getParent != null){
((ViewGroup)mListViews.get(position).getParent()).removeView(mListViews.get(position));
}
container.addView(mListViews.get(position), 0);//添加页卡
return mListViews.get(position);
}
@Override
public int getCount() {
return mListViews.size();//返回页卡的数量
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0==arg1;//官方提示这样写
}
}
需要注意的是:
- 如果你的PagerAdapter仅用于创建View(也就是不使用FragmentPagerAdapter or FragmentStatePagerAdapter),那么完全不需要修改相关代码
- 如果你想把LoopViewPager用于FragmentPagerAdapter or FragmentStatePagerAdapter,必须在adapter中加入一些自定义的改变
- 在显示头尾界面时可能会出现闪烁(譬如你使用了NetworkImageView),你可以通过设置setBoundaryCaching( true ) 来设置缓存,这样头尾界面就不会每次都加载网络数据,而是使用缓存的
三、原理
比如现在有二个View要循环切换,显示的是ONE 和 TWO
| ONE | TWO |
那如何能让它循环呢。其实这时候是用了一个假象:
- 比如TWO按理再往左边移动。这时候我们应该要能看到ONE。这样我们才能感觉这是循环,所以我们再TWO的右边再加一个ONE。
- 同理ONE的界面往右移动也要能看到TWO,所以在ONE的左边加一个TWO
| TWO | ONE | TWO | ONE |
0 1 2 3
//既然我们最左边加了一个<0>位置的TWO。我们原先的ONE就变到了<1>位置,所以在刚开始的时候初始化的位置是1而不是0
- 然后当我们的处于<2>位置的TWO界面朝左边移动的时候,先是能看到<3>位置的ONE了。这时候在划动过程中先给你一种感觉,以为是看到的是<1>位置的ONE
- 然后当划动结束的时候,通过ViewPager.setCurrentItem(1)方法,将页面定位到了<1>位置的ONE,这时候你发现,又可以继续朝右边移动,然后又能看到<2>位置的TWO了
所以,其实划动时候看到的ONE不是你最刚开始看到的<1>位置的ONE界面。但当切换界面的滑动动作全部结束之后。通过ViewPager.setCurrentItem方法,把界面重新移动回到了最刚开始的<1>位置的ONE。
四、 原码分析
这里主要有两个类LoopPagerAdapterWrapper和LoopViewPager
4.1 LoopPagerAdapterWrapper
其实是类似代理模式的实现,LoopPagerAdapterWrapper持有真正的PagerAdapter,但是重写了相关方法来实现数据源的映射关系
public class LoopPagerAdapterWrapper extends PagerAdapter {
//构造函数,既LoopPagerAdapterWrapper里面的mAdapter就是我们传入的PagerAdapter
LoopPagerAdapterWrapper(PagerAdapter adapter) {
this.mAdapter = adapter;
}
//在getCount方法我们发现跟我们前面说的一样,因为要增加头尾二个界面,所以count这时候要在我们传入的PagerAdapter的个数基础上再加上2
@Override
public int getCount() {
return mAdapter.getCount() + 2;
}
//实现映射规则,将其转换为实际的PageAdapter中的显示项
@Override
public Object instantiateItem(ViewGroup container, int position) {
int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter)
? position
: toRealPosition(position);
if (mBoundaryCaching) {
ToDestroy toDestroy = mToDestroy.get(position);
if (toDestroy != null) {
mToDestroy.remove(position);
return toDestroy.object;
}
}
return mAdapter.instantiateItem(container, realPosition);
}
public int toInnerPosition(int realPosition) {
int position = (realPosition + 1);
return position;
}
int toRealPosition(int position) {
int realCount = getRealCount();
if (realCount == 0)
return 0;
int realPosition = (position-1) % realCount;
if (realPosition < 0)
realPosition += realCount;
return realPosition;
}
//实现映射规则
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
int realFirst = getRealFirstPosition();
int realLast = getRealLastPosition();
int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter)
? position
: toRealPosition(position);
if (mBoundaryCaching && (position == realFirst || position == realLast)) {
mToDestroy.put(position, new ToDestroy(container, realPosition,
object));
} else {
mAdapter.destroyItem(container, realPosition, object);
}
}
/*
* 代理模式
*/
@Override
public void finishUpdate(ViewGroup container) {
mAdapter.finishUpdate(container);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return mAdapter.isViewFromObject(view, object);
}
@Override
public void restoreState(Parcelable bundle, ClassLoader classLoader) {
mAdapter.restoreState(bundle, classLoader);
}
@Override
public Parcelable saveState() {
return mAdapter.saveState();
}
@Override
public void startUpdate(ViewGroup container) {
mAdapter.startUpdate(container);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
mAdapter.setPrimaryItem(container, position, object);
}
}
4.1.1 映射规则
- PageAdapter原始的数据源
| A | B | C | D |
0 1 2 3
- LoopPagerAdapterWrapper中的数据源是
| D | A | B | C | D | A |
0 1 2 3 4 5
- 在LoopPagerAdapterWrapper中需要根据当前下标,推算出实际的PageAdapter对应数据下标,映射关系如下
- realadpater.position=(loopadapter.position-1)%count
0->3 D
1->0 A
2->1 B
3->2 C
4->3 D
5->0 A
4.1.2 缓存真实的头尾界面用于显示假循环
- mBoundaryCaching 标示是否需要缓存
如果需要缓存,则
- destroyItem中并不实际销毁,而是放入缓存列表
- instantiateItem中并不新建而是直接拿到数据
private int getRealFirstPosition() {
return 1;
}
private int getRealLastPosition() {
return getRealFirstPosition() + getRealCount() - 1;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
...
if (mBoundaryCaching) {
ToDestroy toDestroy = mToDestroy.get(position);
if (toDestroy != null) {
mToDestroy.remove(position);
return toDestroy.object;
}
}
...
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
...
int realFirst = getRealFirstPosition();
int realLast = getRealLastPosition();
if (mBoundaryCaching && (position == realFirst || position == realLast)) {
mToDestroy.put(position, new ToDestroy(container, realPosition,
object));
} else {
mAdapter.destroyItem(container, realPosition, object);
}
...
}
4.2 LoopViewPager
通过继承Viewpager,并设置了一个内部的PagechangeListener,在onPageScrolled的回调中,发现当内部的pagerAdpater的position滑动到边界的时候,通过调用setCurrentItem,将position又设置到正确的位置
public class LoopViewPager extends ViewPager {
@Override
public void setAdapter(PagerAdapter adapter) {
mAdapter = new LoopPagerAdapterWrapper(adapter);
mAdapter.setBoundaryCaching(mBoundaryCaching);
super.setAdapter(mAdapter);
setCurrentItem(0, false);
}
@Override
public void setCurrentItem(int item) {
if (getCurrentItem() != item) {
setCurrentItem(item, true);
}
}
//setCurrentItem(0)其实应该是setCurrentItem(1)
//因为左边额外加了一个界面(就是上图的<0>位置),所以我们的起始时候是从<1>位置开始。所以如果用户在activity代码里面执行LoopViewPager.setCurrentItem(N, smoothScroll);实际上应该跳到的都是N+1的位置
public void setCurrentItem(int item, boolean smoothScroll) {
int realItem = mAdapter.toInnerPosition(item);
super.setCurrentItem(realItem, smoothScroll);
}
@Override
public int getCurrentItem() {
return mAdapter != null ? mAdapter.toRealPosition(super.getCurrentItem()) : 0;
}
private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
private float mPreviousOffset = -1;
private float mPreviousPosition = -1;
@Override
public void onPageSelected(int position) {
int realPosition = mAdapter.toRealPosition(position);
if (mPreviousPosition != realPosition) {
mPreviousPosition = realPosition;
if (mOuterPageChangeListener != null) {
mOuterPageChangeListener.onPageSelected(realPosition);
}
}
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
int realPosition = position;
if (mAdapter != null) {
realPosition = mAdapter.toRealPosition(position);
if (positionOffset == 0
&& mPreviousOffset == 0
&& (position == 0 || position == mAdapter.getCount() - 1)) {
setCurrentItem(realPosition, false);
}
}
mPreviousOffset = positionOffset;
if (mOuterPageChangeListener != null) {
if (realPosition != mAdapter.getRealCount() - 1) {
mOuterPageChangeListener.onPageScrolled(realPosition,
positionOffset, positionOffsetPixels);
} else {
if (positionOffset > .5) {
mOuterPageChangeListener.onPageScrolled(0, 0, 0);
} else {
mOuterPageChangeListener.onPageScrolled(realPosition,
0, 0);
}
}
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (mAdapter != null) {
int position = LoopViewPager.super.getCurrentItem();
int realPosition = mAdapter.toRealPosition(position);
if (state == ViewPager.SCROLL_STATE_IDLE
&& (position == 0 || position == mAdapter.getCount() - 1)) {
setCurrentItem(realPosition, false);
}
}
if (mOuterPageChangeListener != null) {
mOuterPageChangeListener.onPageScrollStateChanged(state);
}
}
};
}
唯一不足的地方就是需要监听pageChangeListener的pageScroll方法,重新设置position的值(具体的方法是调用scrollTo进行重绘一遍,比较浪费性能)