主要包括以下内容:
- ViewPager 基本使用(简介、适配器)
- ViewPager + TabLayout + Fragment 的使用
- ViewPager 轮播图的使用(指示器、标题、自动轮播、首尾循环)
- ViewPager 的切换效果(PageTransformer)
- ViewPager 切换效果进阶
1.ViewPage的基础使用
常用的方法有以下几个:
setAdapter(PagerAdapter adapter)
设置适配器setOffscreenPageLimit(int limit)
设置缓存的页面个数,默认是 1setCurrentItem(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);
如果页面不是当前显示的页面也不是要缓存的页面,会调用这个方法,将页面销毁。
在引导页中我们常常用 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 中进行恢复创建。
那 Tablayout 如何和 Viewpager 联动呢?由于我们这里主要是讲解 ViewPager 的,所谓 “术业有专攻” 所以关于 TabLayout 的使用我们就不再掺和了。由上总结:
使用 FragmentStatePagerAdapter 更省内存,但是销毁后新建也是需要时间的。一般情况下,如果你是制作主页面,就 3、4 个 Tab,那么可以选择使用 FragmentPagerAdapter,如果你是用于 ViewPager 展示数量特别多的条目时,那么建议使用 FragmentStatePagerAdapter。
第一步,初始化 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)
,这个方法可以实现此种效果,下图可以充分说明