ViewPager在开发中使用的频率非常的高,例如Banner轮播图,首次启动显示的介绍图等,这篇博客就详细介绍一下ViewPager的使用。

PagerAdapter

ViewPagerListView一样,也需要一个Adapter,即PagerAdapter,我们先来看看都有哪些方法可以实现:

public class BannerPageAdapter extends PagerAdapter {
//获取View的总数
@Override
public int getCount() {
return 0;
}
//当ViewPager的内容有所变化时,进行调用。
@Override
public void startUpdate(ViewGroup container) {
super.startUpdate(container);
}
//为给定的位置创建相应的View。创建View之后,需要在该方法中自行添加到container中。
@Override
public Object instantiateItem(ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
//为给定的位置移除相应的View。通常情况下在view不显示的时候调用
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
}
//ViewPager调用该方法来通知PageAdapter当前ViewPager显示的主要项,提供给用户对主要项进行操作的方法。
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
}
//当ViewPager的内容变化结束时,进行调用。当该方法被调用时,必须确定所有的操作已经结束。
@Override
public void finishUpdate(ViewGroup container) {
super.finishUpdate(container);
}
//确认View与实例对象是否相互对应。ViewPager内部用于获取View对应的ItemInfo。
@Override
public boolean isViewFromObject(View view, Object object) {
return false;
}
//保存与PagerAdapter关联的任何实例状态。
@Override
public Parcelable saveState() {
return super.saveState();
}
//恢复与PagerAdapter关联的任何实例状态。
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
super.restoreState(state, loader);
}
//当ViewPager试图确定某个项的位置是否已更改时调用。默认有两个可选项:POSITION_UNCHANGED和POSITION_NONE。
//POSITION_UNCHANGED:给定项的位置未变更
//POSITION_NONE:给定项不再用于PagerAdapter中
//其他值:可以根据具体的情况进行调整
@Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
//新增方法,目前较多用于Design库中的TabLayout与ViewPager进行绑定时,提供显示的标题。
@Override
public CharSequence getPageTitle(int position) {
return super.getPageTitle(position);
}
//获取给定位置的View的显示宽度比例,该比例是相对于ViewPager。
@Override
public float getPageWidth(int position) {
return super.getPageWidth(position);
}
}

其实大部分方法我们都不需要实现。

轮播图

接下来,就基于ViewPager撸一个无限滚动轮播图。先看运行效果:



先说一下我们的大概思路,这里一共创建了三个类,继承于RelativeLayoutBannerView,继承于PagerAdapterBannerPageAdapter和一个继承于Runnable的工具类WeakRunnable,只所以要创建一个WeakRunnable类,是因为防止内存泄露。因为轮播图会无限循环,这里会使用Handler无限执行。然后拦截BannerView触摸事件,来停止或者开启循环。

WeakRunnable

先来看看WeakRunnable

public class WeakRunnable implements Runnable {
public interface WeakRunnableEventListener {
void weakRunnableEventCallBack();
}
private WeakReference<WeakRunnableEventListener> mListener;
public WeakRunnable(WeakRunnableEventListener listener) {
mListener = new WeakReference<WeakRunnableEventListener>(listener);
}
@Override
public void run() {
WeakRunnableEventListener listener = mListener.get();
if (listener != null) {
listener.weakRunnableEventCallBack();
}
}
}

这里的代码很简单,定义了一个接口WeakRunnableEventListener,并且声明一个弱引用,来持有实现了WeakRunnableEventListener接口的类,并且在run方法中判断这个类是否还存在,如果存在,就回调weakRunnableEventCallBack方法五执行具体的任务代码。

BannerPageAdapter

因为ViewPager需要一个PagerAdapter来决定如何显示,所以,我们这里创建类BannerPageAdapter继承于PagerAdapter,代码如下:

public class BannerPageAdapter extends PagerAdapter implements View.OnClickListener {
private List<String> mDataSource;
private Context mContext;
//缓存ImageView
private List<ImageView> mCacheImageViews;
//真实的数据个数
private int mRealSize;
//点击事件回调监听者
private BannerView.BannerItemClickListener mListener;
public void setListener(BannerView.BannerItemClickListener listener) {
mListener = listener;
}
public BannerPageAdapter(Context context) {
mContext = context;
mCacheImageViews = new LinkedList<>();
}
//重载数据源更新方法,并调用父类的方法来刷新数据源
public void notifyDataSetChanged(List<String> dataSource) {
mDataSource = dataSource;
mRealSize = mDataSource.size();
super.notifyDataSetChanged();
}
@Override
public int getCount() {
if (mDataSource == null) return 0;
if (mRealSize == 0) return 0;
return Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
position = position%mRealSize;
ImageView imageView;
//首先去缓存中取ImageView,如果取不到再创建
if (mCacheImageViews.size() == 0) {
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setOnClickListener(this);
}else {
imageView = mCacheImageViews.get(mCacheImageViews.size()-1);
mCacheImageViews.remove(imageView);
}
imageView.setId(position);
String url = mDataSource.get(position);
Glide.with(mContext).load(url).into(imageView);
ViewGroup.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
container.addView(imageView,layoutParams);
return imageView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCacheImageViews.size() > 0) {
mCacheImageViews.clear();
}
container.removeView((View) object);
mCacheImageViews.add((ImageView) object);
}
@Override
public void onClick(View v) {
int position = v.getId();
if (mListener != null) {
mListener.onItemClick(position);
}
}
}

代码也很简单,

在构造方法中,我们初始化了一个用于缓存ImageView的集合。

重写了父类的notifyDataSetChanged,这里我们追加一个数据源集合,并调用父类的方法,来刷新数据。

getCount方法中,如果数据源为空,或者数据源的数量为空,返回0,否则返回最大数,因为我们要做无限轮播。

instantiateItem方法中,首先算出真正的位置,直接position%mRealSize就可以得到当前的位置,然后去缓存集合中取ImageView,如果mCacheImageViews中没有缓存的ImageView,那么直接创建ImageView,并设置点击事件的监听者为自己。如果缓存中有,直接取出来使用,这里使用Glide来加载网络图片,记住,使用Glide就不能使用setTag,这里使用setId。最后一点,一定要将ImageView添加到container中。

destroyItem方法中,我们将销毁的Imageview添加到缓存中,以备下次使用。

最后,在点击事件中,回调监听者。

BannerView

再来看我们的BannerView,继承于RelativeLayout,内部持有一个ViewPager,一个BannerPageAdapter,一个WeakRunnable,代码也很简单:

public class BannerView extends RelativeLayout implements WeakRunnable.WeakRunnableEventListener, ViewPager.OnPageChangeListener {
//每次循环时间
public static int SSUBTIME = 3000;
/**
* 点击监听
*/
public interface BannerItemClickListener {
void onItemClick(int index);
}
private ViewPager mViewPager;
private BannerPageAdapter mAdapter;
private LinearLayout mIndicatorLayout;
private Handler mHandler;
private WeakRunnable mRunnable;
private ArrayList<ImageView> mIndicators;
private int mLastIndex;
private int mRealSize;
public BannerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
initEvent();
}
/**
* 初始化视图
* @param context 上下文
*/
private void initView(Context context) {
mViewPager = new ViewPager(context);
RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addView(mViewPager,params);
mAdapter = new BannerPageAdapter(context);
mViewPager.setAdapter(mAdapter);
mViewPager.addOnPageChangeListener(this);
mIndicatorLayout = new LinearLayout(context);
RelativeLayout.LayoutParams indicatorParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
indicatorParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
indicatorParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
indicatorParams.setMargins(0,0,20,20);
addView(mIndicatorLayout,indicatorParams);
}
/**
* 初始化事件
*/
private void initEvent() {
mHandler = new Handler();
mRunnable = new WeakRunnable(this);
}
/**
* 动态添加指示器
*/
private void addIndicator(int count) {
if (mIndicators == null) {
mIndicators = new ArrayList<>();
}else {
mIndicators.clear();
}
mIndicatorLayout.removeAllViews();
for (int i = 0; i < count; i++) {
ImageView indicatorView = new ImageView(getContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0,0,15,0);
params.gravity = Gravity.CENTER_VERTICAL;
mIndicatorLayout.addView(indicatorView,params);
if (i == 0) {
indicatorView.setBackgroundResource(R.drawable.indicator_selected);
mLastIndex = 0;
}else {
indicatorView.setBackgroundResource(R.drawable.indicator_normal);
}
mIndicators.add(indicatorView);
}
}
/**
* 修改选中的指示器
* @param index 选中
*/
private void changeIndicator(int index) {
if (mLastIndex == index) return;
ImageView lastImageView = mIndicators.get(mLastIndex);
lastImageView.setBackgroundResource(R.drawable.indicator_normal);
ImageView nowImageView = mIndicators.get(index);
nowImageView.setBackgroundResource(R.drawable.indicator_selected);
mLastIndex = index;
}
/**
* 定时回调
*/
@Override
public void weakRunnableEventCallBack() {
int currentIndex = mViewPager.getCurrentItem()+1;
mViewPager.setCurrentItem(currentIndex);
mHandler.postDelayed(mRunnable,SSUBTIME);
}
/**
* 数据源改变
* @param dataSource 数据源
*/
public void notifyDataSetChanged(List<String> dataSource) {
if (dataSource == null) return;
if (dataSource.size() == 0) return;
mRealSize = dataSource.size();
mAdapter.notifyDataSetChanged(dataSource);
mViewPager.setCurrentItem(Integer.MAX_VALUE/2 - Integer.MAX_VALUE/2%dataSource.size());
addIndicator(dataSource.size());
mHandler.postDelayed(mRunnable,SSUBTIME);
}
/**
* 设置点击事件监听者
* @param listener 监听者
*/
public void setOnItemClickListener(BannerItemClickListener listener) {
mAdapter.setListener(listener);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
int index = position % mRealSize;
changeIndicator(index);
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_DOWN) {
mHandler.removeCallbacks(mRunnable);
}else if (MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_UP || MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_CANCEL) {
mHandler.postDelayed(mRunnable,SSUBTIME);
}
return super.dispatchTouchEvent(ev);
}
}

首先,在构造方法中,初始化ViewPager,设置Adapter,初始化事件等。

在重新设置数据源以后,通知Adapter更新数据,并设置ViewPager的当前项。最主要的是onPageSelected方法和dispatchTouchEvent方法,在onPageSelected方法中,我们需要修改当前指示器。在dispatchTouchEvent方法中,我们需要监听触摸手势,如果当前手势为按压,我们要停掉滚动,如果离开,我们再次开启。

Activity中这样使用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.guiyongdong.wangyidemo.MainActivity"
android:orientation="vertical">
<com.guiyongdong.wangyidemo.banner.BannerView
android:layout_width="match_parent"
android:layout_height="150dp"
android:id="@+id/bannerView">
</com.guiyongdong.wangyidemo.banner.BannerView>
</LinearLayout>
public class MainActivity extends AppCompatActivity {
BannerView mBannerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBannerView = (BannerView) findViewById(R.id.bannerView);
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("https://f11.baidu.com/it/u=62466529,3197984907&fm=72");
arrayList.add("https://f11.baidu.com/it/u=1702154046,3138678857&fm=72");
arrayList.add("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2506029705,946578911&fm=23&gp=0.jpg");
arrayList.add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=331414147,1213722392&fm=23&gp=0.jpg");
arrayList.add("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2863772336,3300899200&fm=23&gp=0.jpg");
arrayList.add("https://f11.baidu.com/it/u=62466529,3197984907&fm=72");
arrayList.add("https://f11.baidu.com/it/u=1702154046,3138678857&fm=72");
arrayList.add("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2506029705,946578911&fm=23&gp=0.jpg");
arrayList.add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=331414147,1213722392&fm=23&gp=0.jpg");
arrayList.add("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2863772336,3300899200&fm=23&gp=0.jpg");
mBannerView.notifyDataSetChanged(arrayList);
mBannerView.setOnItemClickListener(new BannerView.BannerItemClickListener() {
@Override
public void onItemClick(int index) {
Toast.makeText(MainActivity.this,""+index,Toast.LENGTH_SHORT).show();
}
});
}
}

至此,一个无限滚动轮播图就好了。

FragmentPageAdapter

通常情况下,除了PagerAdapter,我们还可以使用它的两个子类:FragmentStatePagerAdapterFragmentPagerAdapter,从名字我们就可以看出,这两个适配器主要是针对Fragment的。我们先来看看FragmentPagerAdapter是如何使用的:

public class Main2Activity extends AppCompatActivity {
MyAdapter mAdapter;
ViewPager mPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mAdapter = new MyAdapter(getSupportFragmentManager());
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}
public static class MyAdapter extends FragmentPagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return new MyFragment();
}
@Override
public int getCount() {
return 10;
}
}
public static class MyFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_pager_list,container,false);
return view;
}
}
}

不难看出,FragmentPagerAdapter只需要我们实现三个方法,一个构造方法,一个getCount()返回几项,getItem(int position)返回具体的Fragment。也用法也是很简单的。

FragmentStatePagerAdapterFragmentPagerAdapter用法一样,只是他们内部缓存的策略不是很一样FragmentStatePagerAdapter会在destroyItem中将已经消失的Fragment移除出去,所以他会一直调用getItem来创建,而FragmentPagerAdapter则不会,所以,如果我们的页数比较多的时候,建议使用FragmentStatePagerAdapter,并做适当的缓存策略,防止一直创建Fragment