ViewPager轮播详解

主要包括以下内容:

  • ViewPager 基本使用(简介、适配器)
  • ViewPager + TabLayout + Fragment 的使用
  • ViewPager 轮播图的使用(指示器、标题、自动轮播、首尾循环)
  • ViewPager 的切换效果(PageTransformer)
  • ViewPager 切换效果进阶


1.ViewPage的基础使用

常用的方法有以下几个:

  • setAdapter(PagerAdapter adapter) 设置适配器
  • setOffscreenPageLimit(int limit) 设置缓存的页面个数,默认是 1
  • setCurrentItem(int item) 跳转到特定的页面
  • setOnPageChangeListener(..) 设置页面滑动时的监听器(现在API中建议使用addOnPageChangeListener(..)
  • setPageTransformer(..PageTransformer) 设置页面切换时的动画效果
  • setPageMargin(int marginPixels) 设置不同页面之间的间隔
  • setPageMarginDrawable(..) 设置不同页面间隔之间的装饰图也就是 divide ,要想显示设置的图片,需要同时设置setPageMargin()

2.PageAdapter

PagerAdapter 是抽象的类,所以使用时只能使用它的子类,实现子类必须要实现以下四个方法:

  • getCount(); 是获取当前窗体界面数,也就是数据的个数。
  • isViewFromObject(View view, Object object); 这个方法用于判断是否由对象生成界面,官方建议直接返回return view == object;
  • instantiateItem(View container, int position); 要显示的页面或需要缓存的页面,会调用这个方法进行布局的初始化。
  • destroyItem(ViewGroup container, int position, Object object); 如果页面不是当前显示的页面也不是要缓存的页面,会调用这个方法,将页面销毁。
3.ViewPager + TabLayout + Fragment 的结合使用

在引导页中我们常常用 ViewPager 和 Fragment 结合使用,而像新闻分类的页面我们会再加上一个 TabLayout 三者联动使用。而此时,我们不会再使用 PagerAdapter 了,而是直接使用官方提供的专门用于与 Fragment 结合使用的 FragmentPagerAdapter。

FragmentPagerAdapter 它将每一个页面表示为一个 Fragment,并且每一个 Fragment 都将会保存到 FragmentManager 当中。而且,当用户没可能再次回到页面的时候,FragmentManager 才会将这个 Fragment 销毁。

使用 FragmentPagerAdapter 需要实现两个方法:

  • public Fragment getItem(int position) 返回的是对应的 Fragment 实例,一般我们在使用时,会通过构造传入一个要显示的 Fragment 的集合,我们只要在这里把对应的 Fragment 返回就行了。
  • public int getCount() 这个上面介绍过了返回的是页面的个数,我们只要返回传入集合的长度就行了。

使用起来是非常简单的,FragmentStatePagerAdapter 的使用也和上面一样,那两者到底有什么区别呢?

区别如下:

  • FragmentPagerAdapter:对于不再需要的 fragment,选择调用 onDetach() 方法,仅销毁视图,并不会销毁 fragment 实例。
  • FragmentStatePagerAdapter:会销毁不再需要的 fragment,当当前事务提交以后,会彻底的将 fragmeng 从当前 Activity 的FragmentManager 中移除,state 标明,销毁时,会将其onSaveInstanceState(Bundle outState) 中的 bundle 信息保存下来,当用户切换回来,可以通过该 bundle 恢复生成新的 fragment,也就是说,你可以在onSaveInstanceState(Bundle outState) 方法中保存一些数据,在 onCreate 中进行恢复创建。

由上总结:
使用 FragmentStatePagerAdapter 更省内存,但是销毁后新建也是需要时间的。一般情况下,如果你是制作主页面,就 3、4 个 Tab,那么可以选择使用 FragmentPagerAdapter,如果你是用于 ViewPager 展示数量特别多的条目时,那么建议使用 FragmentStatePagerAdapter。

那 Tablayout 如何和 Viewpager 联动呢?由于我们这里主要是讲解 ViewPager 的,所谓 “术业有专攻” 所以关于 TabLayout 的使用我们就不再掺和了。
第一步,初始化 TabLayout 和 ViewPager 后只要通过调用 TabLayout 的 tabLayout.setupWithViewPager(viewPager) 方法就将两者绑定在一起了。
第二步,重写 PagerAdapter 的 public CharSequence getPageTitle(int position) 方法,而 TabLayout 也正是通过setupWithViewPager() 方法底部会调用 PagerAdapter 中的getPageTitle() 方法来实现联动的

4.ViewPage轮播图


从上图我们可以知道,一般我们使用 ViewPager 做 Banner 时主要有以上几个元素:

4.1 标题 & 指示器
我们可以把标题和指示器直接写在我们 Banner 的 item 的布局中,这样通过在 PageAdapter 的 instantiateItem() 方法初始化页面时,直接设置。但是一般我们不会这样做(如果标题没有阴影的话,可以如上面说的那样),因为这样在页面滑动的时候,会显得特别生硬,尤其是指示器。
那该如何呢?一般我们会在 ViewPager 所在的布局文件中,声明指示器和标题布局,如下:

<FrameLayout
     ...>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        ..."/>

    <LinearLayout
        android:layout_gravity="bottom"
        ...>
        <!--指示器布局,因为不知道 item 的个数,所以会动态的把指示器的View添加到这里-->
        <LinearLayout
            android:id="@+id/bannerIndicators"
            .../>
        <!--标题-->
        <TextView
            android:id="@+id/bannerTitle"
            .../>
    </LinearLayout>

</FrameLayout>
那如何才能实现当页面滑动时,标题和指示器伴随改变呢?还记不记得,一开始介绍 ViewPager 时,它有一个可以设置监听页面改变的方法 addOnPageChangeListener(),在 OnPageChangeListener 监听器中有一个页面滑动结束时的回调方法 onPageSelected(int position) ,我们只需要在这个方法中,来设置标题和指示器跟随变化就行了

4.2自动轮播

实现自动轮播的原理其实更简单,只要我们每隔一定时间发送一个切换页面的事件就行了。最方便的是使用Handler,Handler.sendEmptyMessageDelayed(int what, long delayMillis)

Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (mAutoPlay) {
                //mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//无限轮播时
                mViewPager.setCurrentItem((mViewPager.getCurrentItem()+1) % mViewPagerItemCount)
                this.sendEmptyMessageDelayed(MSG_WHAT, delayMillis);
            }
        }
    };

初始化完成后调用一次mHandler.sendEmptyMessageDelayed(MSG_WHAT, delayMillis);

4.3 首尾循环无限轮播

无限轮播是指:当我们手动滑到最后一个页面时,依然可以向后伴随手指滑动,并跳转显示的是第一个页面;反之滑到首个页面也是一样。主要有以下两种方法:

1.过设置ViewPager展示的个数是Integer.MAX_VALUE来搞定

a.在PagerAdapter中getCount方法中设置当前展示条目总个数

@Override
    public int getCount() {
        //设置为展示好多条目
        return Integer.MAX_VALUE;
    }
b.在PagerAdapter中instantiateItem中展示条目信息

@Override
    public Object instantiateItem(ViewGroup container, int position) {
        //getView 
        View view = View.inflate(context, R.layout.vp_item, null);
        ImageView imageView = (ImageView) view.findViewById(R.id.imageView);
        TextView tv_title = (TextView) view.findViewById(R.id.tv_title);
        // 0 1 2 3 4 5 6 7
        // 0 1 2 3 0 1 2 3
        //设置图片资源,设置时,由于图片索引值从0---Integer.MAX_VALUE,所以在这里进行索引%集合.size()
        imageView.setImageResource(newsList.get(position%newsList.size()).getResId());
        tv_title.setText(newsList.get(position%newsList.size()).getTitle());
        //添加到容器中
        container.addView(view);
        return view;
    }
c.在Activity中设置初始页

//设置数据适配器
        viewPager.setAdapter(new MyPagerAdapter(this, newsList));
        //设置当前页码值--一开始就在某位置
        viewPager.setCurrentItem(10000*newsList.size());
2. 在添加集合时,先往集合最前边添加最后一个条目,然后正常添加集合条目,再在最后边添加第一个条目,如当前有数据123,则设置页面为31231


其实这个思路很简单:
1. 添加最后一条数据到第一条,添加第一条数据到最后一条;
2. 设置监听器;
3. 设置初始化时设置当前页面为第二页

这种方法的缺点是,当滑动到第一页和最后一页的时候会出现跳动的现象。那么出现这个问题的原因是什么呢?通过在onPageSelected(int position)函数中打印log信息,我们发现这个函数在viewpager滑动动画还没结束的时候就已经被调用了,所以在这里调用setCurrentItem方法会强制取消当前正在进行的动画并跳转。
那么相应的,我们可以想到解决方法:
1. 在onPageSelected(int position)方法中记录被选中的页面;
2. 在onPageScrollStateChanged(int state)判断当前动画是否结束,当动画结束时调用setCurrentItem方法跳转页面。

public abstract class LoopVPAdapter<T> extends PagerAdapter implements ViewPager.OnPageChangeListener{

//    当前页面
    private int currentPosition = 0;

    protected Context mContext;
    protected ArrayList<View> views;
    protected ViewPager mViewPager;

    public LoopVPAdapter(Context context, ArrayList<T> datas, ViewPager viewPager) {
        mContext = context;
        views = new ArrayList<>();
//        如果数据大于一条
        if(datas.size() > 1) {
//            添加最后一页到第一页
            datas.add(0,datas.get(datas.size()-1));
//            添加第一页(经过上行的添加已经是第二页了)到最后一页
            datas.add(datas.get(1));
        }
        for (T data:datas) {
            views.add(getItemView(data));
        }
        mViewPager = viewPager;
        viewPager.setAdapter(this);
        viewPager.addOnPageChangeListener(this);
        viewPager.setCurrentItem(1,false);
    }

    @Override
    public int getCount() {
        return views.size();
    }

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

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

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

    protected abstract View getItemView(T data);

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
        currentPosition = position;
    }

    @Override
    public void onPageScrollStateChanged(int state) {
//        若viewpager滑动未停止,直接返回
        if (state != ViewPager.SCROLL_STATE_IDLE) return;
//        若当前为第一张,设置页面为倒数第二张
        if (currentPosition == 0) {
            mViewPager.setCurrentItem(views.size()-2,false);
        } else if (currentPosition == views.size()-1) {
//        若当前为倒数第一张,设置页面为第二张
            mViewPager.setCurrentItem(1,false);
        }
    }


}
关键代码:

 @Override
    public void onPageSelected(int position) {
        currentPosition = position;
    }

    @Override
    public void onPageScrollStateChanged(int state) {
//        若viewpager滑动未停止,直接返回
        if (state != ViewPager.SCROLL_STATE_IDLE) return;
//        若当前为第一张,设置页面为倒数第二张
        if (currentPosition == 0) {
            mViewPager.setCurrentItem(imageViews.size()-2,false);
        } else if (currentPosition == imageViews.size()-1) {
//        若当前为倒数第一张,设置页面为第二张
            mViewPager.setCurrentItem(1,false);
        }
    }
4.4 自定义ViewPage的切换效果

关于 ViewPager 的切换动画,官方提供了一个内部接口 ViewPager.PageTransformer 来供我们实现自定义切换动效。这个接口里只提供了一个方法 public void transformPage(View view, float position)
transformPage 方法两个参数,一个是 View ,这个好理解就是当前要设置动效的页面。这个页面并不单单是指当前显示的页面,即将滑出的页面、即将滑入的页面、已经隐藏的页面,也就是说这个 View 是指所有的页面。那如何分辨 View 到底是指哪个页面呢,这个需要根据第二个参数 position 来辨别。
position 并不是ViewPager 页面的下标,从 doc 注释来看,当前选中的 item 的 position 永远是 0 ,被选中 item 的前一个为 -1,被选中 item 的后一个为 1。其实这里文档的描述并不是完全正确的,前后 item position 为 -1 和 1 的前提是你没有给 ViewPager 设置 pageMargin。
如果你设置了 pageMargin,前后 item 的 position 需要分别加上(或减去,前减后加)一个偏移量(偏移量的计算方式为 pageMargin / pageWidth)。

在用户滑动界面的时候,position 是动态变化的,下面以左滑为例(以向左为正方向):

  • 选中 item 的 position:从 0 渐至 -1 - offset (pageMargin / pageWidth)
  • 前一个 item 的 position:从 -1 渐至 -1 - offset (pageMargin / pageWidth)
  • 前两个 item 的 position:从 -2 渐至 -2 - offset (pageMargin / pageWidth),再往前就以此类推
  • 后一个 item 的 position:从 1 + offset (pageMargin / pageWidth) 渐至 0,再往后就以此类推
这里给大家举一个视差切换动效的实现方式,其实实现起来很简单,就是滑动时给页面再设置一个页面横向滑动的动画,让页面实现滑动的速度慢于手指滑动的速度,这样就会有种视差的效果

@Override
public void transformPage(View page, float position) {
    int width = page.getWidth();
    //我们给不同状态的页面设置不同的效果
    //通过position的值来分辨页面所处于的状态
    if (position < -1) {//滑出的页面
        page.setScrollX((int) (width * 0.75 * -1));
    } else if (position <= 1) {//[-1,1]
        if (position < 0) {//[-1,0]
            page.setScrollX((int) (width * 0.75 * position));
        } else {//[0,1]
            page.setScrollX((int) (width * 0.75 * position));
        }
    } else {//即将滑入的页面
        page.setScrollX((int) (width * 0.75));
    }
}

由上图可以看出,当滑动时,(如果没有偏移量)界面上最多出现两个 item,一个即将滑出即将隐藏的页面(postion变化为:从0渐到-1),一个滑入即将完全显示的页面(postion变化为:从1渐到0)

4.5 进阶: 在一个viewpage页面看到多个item

设置的方法就剩 setPageMargin(int marginPixels) ,这个方法可以实现此种效果,下图可以充分说明





  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值