(LoopingViewPager) 可循环的 ViewPager 实现及详细分析

3,418 阅读10分钟

前提:前几天无聊,下了个《读者》app,然后正好使用的过程中发现发现从文字列表点击进去后可以查看具体的文章内容,然后再文章内容中还可以左划右划来实现文章的切换,然后想到应该是ViewPager+fragment来实现的,前面用过这个ViewPager,但是没有好好看过,所以今天周六抽空好好研究了下关于ViewPager这一块,特别是循环,也就是当你划到最后一个界面,再划的时候,可以回到第一个界面。

-----------------------------基础使用知识讲解分割君---------------------------------------

关于ViewPager的基础讲解的内容我这边就引用其他大神的文章里面的内容。
在此感谢简书的Carson_Ho

-------------------------------ViewPager循环切换分割线------------------------------

好了,现在开始我是怎么去学循环切换的。

首先第一步:百度搜索(好吧,我没用谷歌,不是找bug的解决方法。百度够了)。

网上我看到有一种是设置PagerAdapter里面的getCount设置为Integer.MAX_VALUE。然后再设置。其实是让界面类似接近(无限多),反正客户不会吃饱没事干不停往后面划动。但我觉得这样不是特别好。所以没使用。
大家也可以看下实现方式,不过不管怎么样。能实现就都是棒棒哒💯
感谢简书的violinlin
Banner的封装--实现ViewPager的循环轮播效果

我百度过后,在github中看到一个关于ViewPager可循环切换的别人封装好的自定义ViewPager:LoopingViewPager。
github.com/imbryk/Loop…

拉到网页最下面写着别人的例子里面也用到的这个的LoopingViewPager的链接,啥 Jake Wharton都用了?那我就没犹豫,马上尝试体验下这个了。


按照github中作者提到的,当前循环分为二种情况,一种是用在ViewPager里面装的是View,然后View来循环,还有一种是ViewPager里面是Fragment,然后Fragment的循环


Paste_Image.png

在研究前我们要先学会使用

里面一共就二个自定义文件:LoopViewPager.java和LoopPagerAdapterWrapper.java,分别继承了ViewPager.java 和PagerAdapter.java

我们先讲ViewPager里面是View的循环

View循环特别方便。我们需要把我们Activity中的

标签替换成标签。


布局ViewPager替换

比如我现在是二个View的切换,二个View分别是加载下图的那个布局


第一个View 的布局

第二个View的布局

这个我们自定义的继承PagerAdapter的ViewAdapter类,如果是按照
Android开发:ViewPage详细使用教程里面的教程写的。那instantiateItem方法和destoryItem方法先改成我下面图片那样。不然等会循环的时候会报错。原因后面我会解释


ViewAdapter.java

activity代码

然后可以看到效果:


Paste_Image.png

Paste_Image.png

然后手指继续往左滑动,会送Fragment_TWO 又回到了Fragment_ONE的界面。

SO ------ WHY ?

我先来讲解一下大概思路。这样大家后面看讲解的时候就会更容易理解,
比如现在有二个View要循环切换,显示的是ONE 和 TWO


ONE和TWO二个界面

那如何能让它循环呢。其实这时候是用了一个假象。
比如TWO按理再往左边移动。这时候我们应该要能看到ONE。这样我们才能感觉这是循环,所以我们再TWO的右边再加一个ONE。同理ONE的界面往右移动也要能看到TWO,所以在ONE的左边加一个TWO。


变为四个界面

既然我们最左边加了一个<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。

------------------------------------源码分析分割线----------------------------------------

因为我们只是把的v4包下的ViewPager替换成了LoopViewPager。所以我们先看LoopViewPager在执行setAdapter()方法之后到底做了什么处理。

@Override
    public void setAdapter(PagerAdapter adapter) {
        mAdapter = new LoopPagerAdapterWrapper(adapter);//第一步
        mAdapter.setBoundaryCaching(mBoundaryCaching);//第二步
        super.setAdapter(mAdapter);//第三步
        setCurrentItem(0, false);//第四步
    }

我们一步步来分析:

第一步:

把我们传入的PagerAdapter再传入到自定义的LoopPagerAdapterWrapper中,进行封装,因为LoopPagerAdapterWrapper本身也是继承PagerAdapter的。所以等会真正给ViewPager设置adapter的时候已经变为了经过LoopPagerAdapterWrapper封装过的adapter了。具体封装等会再分析。

第二步:
/**
     * If set to true, the boundary views (i.e. first and last) will never be destroyed
     * This may help to prevent "blinking" of some views 
     * 
     * @param flag
     */
    public void setBoundaryCaching(boolean flag) {
        mBoundaryCaching = flag;
        if (mAdapter != null) {
            mAdapter.setBoundaryCaching(flag);
        }
    }

主要是用来设置是否第一个和最后一个view要缓存,不去销毁。而第一个和最后一个你懂得。就是我们为了循环效果而写的那二个界面。因为跟循环的原理关系不是很大。所以这里就不多介绍了。

第三步:

把我们上面经过LoopPagerAdapterWrapper封装过的adapter。赋予给ViewPager。

第四步:

LoopViewPager的setCurrentItem方法代码

public void setCurrentItem(int item, boolean smoothScroll) {
        int realItem = mAdapter.toInnerPosition(item);
        super.setCurrentItem(realItem, smoothScroll);
    }

而LoopPagerAdapterWrapper 的toInnerPosition方法:

 public int toInnerPosition(int realPosition) {
        int position = (realPosition + 1);
        return position;
    }

没错,就是我前面提到的,因为左边额外加了一个界面(就是上图的<0>位置),所以我们的起始时候是从<1>位置开始。所以如果用户在activity代码里面执行LoopViewPager.setCurrentItem(N, smoothScroll);实际上跳到的都是N+1的位置。

好了,接下来我们来看第一步中。LoopPagerAdapterWrapper把我们传入的PageAdapter进行封装,到底做了什么处理。

我们知道继承PagerAdapter,一般是要实现以下几个方法

  • 构造函数
  • getCount
  • instantiateItem
  • destroyItem
  • isViewFromObject

我们就这几个主要方法一一来看。

构造函数:
//构造函数,既LoopPagerAdapterWrapper里面的mAdapter就是我们传入的PagerAdapter
LoopPagerAdapterWrapper(PagerAdapter adapter) {
        this.mAdapter = adapter;
    }
getCount:

然后在getCount方法我们发现跟我们前面说的一样,因为要增加头尾二个界面,所以count这时候要在我们传入的PagerAdapter的个数基础上再加上2。

@Override
public int getCount() {
     return mAdapter.getCount() + 2;
}
instantiateItem:
    @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);
    }

我们发现最后调用的是我们自己的那个mAdapter的instantiateItem方法,而传入的第二个参数realPosition被经过处理,即:

 int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter)
                ? position
                : toRealPosition(position);

因为我们当前先展示的是View界面的循环切换,所以最后是
int realPosition = toRealPosition(position);

我们再看toRealPosition方法到底对我们的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;
    }

public int getRealCount() {    
       return mAdapter.getCount();
}

所以我产生以下理解:

所以就是说我们在比如显示LoopPagerAdapterWrapper的第一个界面的时候。其实是调用我们自己写的PagerAdapter来创建界面,然后创建的是自己写的PagerAdapter的最后一个界面。这样肯定需要一个公式来对应。

就拿现在这个四个界面来写说。创建第一个界面时候。是在LoopPagerAdapterWrapper里面position是0,因为是为了实现循环,所以理论上是要显示TWO这个界面。但是因为最后是用自己写的PagerAdapter来进行创建,也就是我们的adapter中的position为1,才是TWO这个界面,

我们知道我们其实只想要二个界面,也就是ONE和TWO(即你自己写的Adapter中的<0>和<1>二个界面),但为了实现循环,其实偷偷的给我们制造了四个界面(即《0》,《1》,《2》,《3》四个界面)。
我用《》和<>分别代表二个Adapter中的界面的position。
所以对应的关系是上面那个toRealPosition的算法。

具体来看就是:

实际四个界面: 《0》 《1》 《2》 《3》
想要的二个界面: <1> <0> <1> <0>
扩展:

如果我们想要的是四个界面,我们自己写的PagerAdapter中分别显示文字ONE,TWO,THREE,FOUR。就是position为0-3。为了循环,我们的PagerAdapter会用LoopPagerAdapterWrapper来封装,会增加二个位置,LoopPagerAdapterWrapper的position就变成了0-5。

实际六个界面: 《0》 《1》 《2》 《3》 《4》 《5》
想要的四个界面: <3> <0> <1> <2> <3> <0>

所以这就好理解了。比如在LoopPagerAdapterWrapper的instantiateItem方法里面的position要转换过后,再传给自己写的PagerAdapter的instantiateItem方法里面。

通过上面的提到过的toRealPosition方法,我们发现就可以把数字进行转换。
0-->3 , 1-->0 , 2-->1 , 3-->2, 4-->3 , 5-->0。

destroyItem:
@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);
        }
    }

这时候看起来是不是和上面的instantiateItem方法差不多。哈哈。估计大家这时候应该都看得懂了。我也不多做分析了。😜

isViewFromObject:
@Override
public boolean isViewFromObject(View view, Object object) {    
         return mAdapter.isViewFromObject(view, object);
}

就是调用自己写的PagerAdapter的isViewFromObject方法。

好的,这样大概就知道了LoopPagerAdapterWrapper对我们的自定义的PagerAdapter做了哪些封装处理。那当我们滑到最后一个,再滑动就会自动回到第一个是如何实现的?我们继续分析下去

如何循环从最后回到开始

我们前面提过。比如


四个界面

从位置2的的TWO的界面再向左边移动的时候,滑动过程显示位置3的ONE,然后滑动结束后。实际上是通过ViewPager的setCurrentItem方法跳转到了位置1的ONE。

因为LoopViewPager是继承ViewPager。我们来看LoopViewPager的源码做了什么处理:

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);
            }
        }
    };

}

这个接口不知道的可以再看一遍以下这篇文章。
Android开发:ViewPage滑动接口最详细解析

根据上面代码我们可以看在,在LoopViewPager中自定义了OnPageChangeListener接口,然后赋值给了LoopViewPager。所以在LoopViewPager在滑动的时候会调用它的onPageSelected,onPageScrolled,onPageScrollStateChanged方法。

在onPageScrolled方法里面

if (mAdapter != null) {
        realPosition = mAdapter.toRealPosition(position);
        if (positionOffset == 0
                && mPreviousOffset == 0
                && (position == 0 || position == mAdapter.getCount() - 1)) {
            setCurrentItem(realPosition, false);
        }
}

和onPageScrollStateChanged里面的

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);
        }
}

这下就知道了吧。这下就知道了为啥最后又能回到前面的界面去了。哈哈

-----------------------------------先结尾分割线割一下----------------------------------

文章发现好长啊。View+ViewPager讨论先到这里。后面再补上Fragment+ViewPager的讨论