ViewPager 无限循环的两种实现方式

以前第一次学 ViewPager 的时候其实老师就讲了如何实现 ViewPager 的无限循环,以及自动播放、添加小圆点等,但是在后来的项目中却没有实际用到过这些功能,导致都有些忘了。前几天看到一个效果:



这个效果 Android  有个控件叫 Gallery 挺好实现这种效果,但是在 API 16 的时候这个控件就被弃用了,现在基本都是用 ViewPager 来实现图片的轮播了,先不说上面的 ViewPager 跟平时我们见到的 ViewPager 有些不同(它两边可以看到前后各一个页卡),先来说说 ViewPager 的基本使用、实现无限循环和实现自动播放。

基本使用

首先在布局中放一个 ViewPager(v4 包下的ViewPager):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.qinshou.qsviewpager.MainActivity">

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_nba"
        android:layout_width="match_parent"
        android:layout_height="240dp" />

</RelativeLayout>

然后在 MainActivity 中初始化 ViewPager:
public class MainActivity extends AppCompatActivity {
    //图片资源id数组
    private int[] resID = new int[]{R.mipmap.carter, R.mipmap.iverson, R.mipmap.jordan, R.mipmap.kobe, R.mipmap.macgrady};
    private List<ImageView> imageViewList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化一个集合,用来存放ViewPager每个页卡里面的控件
        imageViewList = new ArrayList<ImageView>();
        //初始化ViewPager每个页卡的控件,设置内容并添加到集合中
        for (int i = 0; i < resID.length; i++) {
            ImageView mImageView = new ImageView(this);
            mImageView.setScaleType(ImageView.ScaleType.FIT_XY);
            mImageView.setImageResource(resID[i]);
            imageViewList.add(mImageView);
        }
        ViewPager vpNBA = (ViewPager) findViewById(R.id.vp_nba);
        //设置适配器
        vpNBA.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return imageViewList.size();
            }

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

            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                container.addView(imageViewList.get(position));
                return imageViewList.get(position);
            }

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView(imageViewList.get(position));
            }
        });
    }
}

运行程序,就可以左右滑动页卡了:


我们在拿到 ViewPager 后需要为其设置适配器,在这个适配器中我们必须至少要重写四个方法:
getCount():返回当前有效视图的个数,就是用来计算ViewPager页卡个数的。
isViewFromObject():这个方法是用来判断 instantiateItem() 方法所返回的 Object对象 与一个 View 对象是否是代表的同一个对象(即是否是同一个 View )。
instantiateItem():创建指定位置的页面视图,适配器有责任将即将创建的View视图添加到给定的container中。
destroyItem():该方法实现的功能是移除一个给定位置的页面,适配器有责任从容器中删除这个视图。
这四个方法是一定要重写的,如何重写,参照上面代码。

无限循环

在上面的程序中,当我们滑动到第一个页卡时发现不能再往左滑了,到最后一个页卡时发现不能再往右滑了,这样感觉不太友好,我们所见的视图循环展示的效果中,一般都是可以循环的。正常来说,当页卡数大于 3 个时,才有实现循环的必要,ViewPager 的无限循环通常有两种实现方式。

第一种

我们需要在原来的页卡基础上再添加两个页卡,第一个页卡前再加入最后一个页卡,最后一个页卡后再添加第一个页卡,即原来的页卡结构是 ABCDE,我们重新定义为EABCDEA,然后为 ViewPager 添加监听器,在监听到相应事件后再做一下处理:
public class MainActivity2 extends AppCompatActivity {
    //图片资源id数组,需要在第一张图片前添加最后一张图片,最后一张图片后添加第一张图片
    private int[] resID = new int[]{R.mipmap.macgrady, R.mipmap.carter, R.mipmap.iverson, R.mipmap.jordan, R.mipmap.kobe, R.mipmap.macgrady, R.mipmap.carter};
    private List<ImageView> imageViewList;
    private ViewPager vpNBA;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化一个集合,用来存放ViewPager每个页卡里面的控件
        imageViewList = new ArrayList<ImageView>();
        //初始化ViewPager每个页卡的控件,设置内容并添加到集合中
        for (int i = 0; i < resID.length; i++) {
            ImageView mImageView = new ImageView(this);
            mImageView.setScaleType(ImageView.ScaleType.FIT_XY);
            mImageView.setImageResource(resID[i]);
            imageViewList.add(mImageView);
        }
        vpNBA = (ViewPager) findViewById(R.id.vp_nba);
        //设置适配器
        vpNBA.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return imageViewList.size();
            }

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

            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                container.addView(imageViewList.get(position));
                return imageViewList.get(position);
            }

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView(imageViewList.get(position));
            }
        });
        vpNBA.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            @Override
            public void onPageSelected(int position) {
                if (imageViewList.size() > 1) { //多于1,才会循环跳转
                    if (position == 0) {
                        //如果当前页卡为第一个页卡,即在原始数据的第一个再往左滑时,切换到倒数第二个页卡,即原始数据的最后一个
                        position = imageViewList.size() - 2;
                        vpNBA.setCurrentItem(position);
                    } else if (position == imageViewList.size() - 1) {
                        //如果当前页卡为最后一个页卡,即在原始数据的最后一个再往右滑时,切换到第二个页卡,即原始数据的第一个
                        position = 1;
                        vpNBA.setCurrentItem(position);
                    }
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
        vpNBA.setCurrentItem(1);
    }
}

OnPageChangeListener() 有三个回调方法:onPageScrolled()、onPageSelected()、onPageScrollStateChanged(),它们分别在页卡滑动、页卡选择后、页卡滑动状态改变的时候调用,至于它们的调用顺序和更多的用法,这里先不做深究。我们在页卡选择后,如果是第一个页卡,我们添加的 E 页卡(第一个 E )时,调用 ViewPager 的 setCurrentItem() 方法跳转到倒数第二个页卡,正常的 E 页卡(第二个 E),如果是最后一个页卡,我们添加的 A 页卡 (第二个 A )时,跳转到第二个页卡,正常的 A 页卡(第一个 A )。
为了不显示第一个 E 页卡,我们应该从第一个 A 页卡开始显示。
运行程序,效果如下:


可以看到,这样虽然实现了无限循环,但是在最后一个页卡跳转到第一个页卡和第一个页卡跳转到最后一个页卡的时候,会有好几张图片一闪而过的感觉,setCurrentItem() 有两个重载方法:setCurrentItem(int item) 和 setCurrentItem(int item, boolean smoothScroll),第二个参数看参数名就能知道是平滑滑动的意思,传入 true 则会有切换时的动画效果,那么我们可以传入 false 取消切换时动画效果,重新运行,效果如下:


gif 图看着效果要好一点,但是实际上最后一个页卡和第一个页卡之间的切换会因此比其他页卡之间的切换要显得快一点,这是方式一的一个不足。

方式二

我们不需要对数据原来的结构做任何处理,而在添加时适配器的时候,将页卡数量设置为最大值(Integer.MAX_VALUE),并且在创建页卡视图和销毁页卡视图的时候对下标坐一下处理,最后让 ViewPager 从中间开始显示(为了一开始能往左滑动)即可:
public class MainActivity extends AppCompatActivity {
    //图片资源id数组,不需要做任何处理
    private int[] resID = new int[]{R.mipmap.macgrady, R.mipmap.carter, R.mipmap.iverson, R.mipmap.jordan, R.mipmap.kobe, R.mipmap.macgrady, R.mipmap.carter};
    private List<ImageView> imageViewList;
    private ViewPager vpNBA;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化一个集合,用来存放ViewPager每个页卡里面的控件
        imageViewList = new ArrayList<ImageView>();
        //初始化ViewPager每个页卡的控件,设置内容并添加到集合中
        for (int i = 0; i < resID.length; i++) {
            ImageView mImageView = new ImageView(this);
            mImageView.setScaleType(ImageView.ScaleType.FIT_XY);
            mImageView.setImageResource(resID[i]);
            imageViewList.add(mImageView);
        }
        vpNBA = (ViewPager) findViewById(R.id.vp_nba);
        //设置适配器
        vpNBA.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return Integer.MAX_VALUE;
            }

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

            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                container.addView(imageViewList.get(position % imageViewList.size()));
                return imageViewList.get(position % imageViewList.size());
            }

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView(imageViewList.get(position % imageViewList.size()));
            }
        });
        vpNBA.setCurrentItem(Integer.MAX_VALUE / 2);
    }
}

运行程序,效果如下:


在视觉效果上要比方式一好一些的。其实方式二并不是真正的无限循环,因为 Integer.MAX_VALUE 也是有固定值的,是 2 的 31 次方 -1 ,如果往一个方向滑动这么多次的话,还是会到头的,不过即使让它从中间开始显示,一般也没有人有那个闲心和时间滑动到头的,所以一般推荐方式二。

自动播放

其实很简单,我们只需要再开一个子线程,让这个子线程每隔两秒就去切换 ViewPager 的页卡即可,注意要在程序退出的时候将 isLoop 标志位置为 false 退出循环。
        //  循环自动播放
        new Thread() {
            public void run() {
                isLoop = true;
                while (isLoop) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    // 运行到UI 线程的Thread
                    runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
                            // TODO Auto-generated method stub
                            vpNBA.setCurrentItem(vpNBA.getCurrentItem() + 1);
                        }
                    });
                }
            }
        }.start();

扩展

无限循环和自动播放的基本方式就是这样了,现在来看看如何实现文章开头的哪种效果,它与普通的 ViewPager 不同的时候可以看到左右两边的页卡,其实很好实现,首先,我们让 ViewPager 距左边和右边稍微有点距离,但是这样还并不能让它显示左右两个页卡。不要方,ViewGroup 在 xml 布局中 有一个属性:android:clipChildren,默认为true,它的意思是是否限制子View在其范围内。所以我们只需要在 ViewPager 所在的 Layout 和 ViewPager 中同时设置(一定要两个都设置)该属性为 false 即可:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false"
    tools:context="com.qinshou.qsviewpager.MainActivity">

    <android.support.v4.view.ViewPager
        android:id="@+id/vp_nba"
        android:layout_width="match_parent"
        android:layout_height="240dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:clipChildren="false" />

</RelativeLayout>

运行程序,效果如下:


可以看到左右各显示了一个页卡的部分内容,但是在向右切换的时候右边边界为白色,向左切换的时候左边边界为白色,显然这不理想,我们需要设置 ViewPager 的页卡预加载个数:
vpNBA.setOffscreenPageLimit(2);
设置为 2 即可。这样设置就可以消除白色边界,不过往右滑的时候没事,一旦往左滑,程序崩溃,会报一个异常:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
这是在适配器中创建视图的时产生的异常,为什么会产生这个异常的原因我倒还没弄懂,不过它的意思是这个子 View 已经有 Parent 了,必须先调用 removeView() 方法,所以需要修改一下适配器:
        vpNBA.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return Integer.MAX_VALUE;
            }

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

            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                //java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
                //为了解决上面的异常,需要加入下面的if判断,并且destroyItem()方法不需要调用removeView()了
                if (imageViewList.get(position % imageViewList.size()).getParent() != null) {
                    ((ViewPager) imageViewList.get(position % imageViewList.size()).getParent()).removeView(imageViewList.get(position % imageViewList.size()));
                }
                container.addView(imageViewList.get(position % imageViewList.size()));
                return imageViewList.get(position % imageViewList.size());
            }

            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
//                container.removeView(imageViewList.get(position % imageViewList.size()));
            }
        });

最后,我们为 ViewPager 一个切换动画来达到左右页卡显示的部分比当前页卡要短一截的效果:
public class CustomTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.8F;

    @Override
    public void transformPage(View page, float position) {
        if (position < -1) {
            page.setScaleY(MIN_SCALE);
        } else if (position <= 1) {
            float scale = Math.max(MIN_SCALE, 1 - Math.abs(position));
            page.setScaleY(scale);
        } else {
            page.setScaleY(MIN_SCALE);
        }
    }

}

        vpNBA.setPageTransformer(true, new CustomTransformer());

运行程序,最终效果如下:



总结

最后发现,实现扩展效果那么预加载个数跟图片数量还有关系,所以这个就需要具体情况具体分析了。虽然在实际项目中还没有用到 ViewPager ,但是不代表今后用不到,有备无患嘛,其实这两种方式孰优孰劣,对内存性能又有何影响,都是值得深究的,还有 ViewPager 切换时的自定义动画,都是可以自行发掘的,也希望今后可以自己做出很好看的 UI 效果。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值