什么是ViewPager?
ViewPager是一个布局容器,位于Android Support v4包中:
android.support.v4.view.ViewPager,本身实际上是一个特别的ViewGroup。它允许用户通过左右滑动翻页的操作来实现数据展示的变化(实际上可以修改后实现竖直方向的上下翻页)。ViewPager需要设置由开发者实现的适配器PagerAdapter,以此来决定在每个位置上该展示什么样的数据。
实际开发中,ViewPager是一个很重要的控件,比较常见的场景有:大量并列的不同分类的列表页面(如新闻类APP)、广告轮播图、图片照片展示画廊等等。
ViewPager比较重要的一些常用方法
- setAdapter(PagerAdapter)
把ViewPager与适配器进行绑定,参数可以为null(会清空原适配器及相应的数据和各种视图对象)。
- addOnPageChangeListener(ViewPager.OnPageChangeListener)
给ViewPager添加一个监听,当ViewPager处于滑动状态的改变、滑动中、滑动结束后时,OnPageChangeListener接口的3个方法分别会得到回调,开发者就可以以此做不少事情了。如果不再需要监听,则可以通过removeOnPageChangeListener(ViewPager.OnPageChangeListener)或clearOnPageChangeListeners()方法来清除。
- setCurrentItem(int)、setCurrentItem(int, boolean)
控制ViewPager直接跳转到指定位置的页面。
- getCurrentItem()
获取当前索引,即当前显示的页面在数据List中的位置。
- setPageTransformer (boolean, ViewPager.PageTransformer)、setPageTransformer (boolean, ViewPager.PageTransformer, int)
设置滑动动画,即从当前位置向左或向右滑动时,会添加一个动画效果,如翻转、渐近渐出等效果。
- setOffscreenPageLimit(int)
设置当前页面左右两侧,每一侧应该缓存的页面数量。默认值为1,如果你设置的参数小于1,也会被强制改为1。不难理解,ViewPager在滑动时,至少需要当前页面、左1、右1这3个页面(如果当前页面是第0个,则是当前页面、右1共2个页面),左右滑动操作才会流畅、不显得空白一片。
简单来说呢,比如你的ViewPager一共有10个页面,你当前位于第6个页面。那么当你设置为3的时候,则位于左侧的第3、4、5页面,与位于右侧的第7、8、9页面,都处于已经缓存的状态,不会被回收掉,而随着滑动操作,当滑动到下一个页面的时候,该页面因为已经创建过并缓存好了,所以也不会需要重新创建。
由于Fragment有很完善的生命周期,当ViewPager的页面是Fragment的时候,这个方法会显得特别重要。
- setPageMargin(int marginPixels)
设置相邻页面之间的间距。默认情况下是0,所以相邻的页面彼此之间是连在一起,没有间距的。从参数名来看,不难理解该参数的单位是像素px。
- setPageMarginDrawable(Drawable)、setPageMarginDrawable(@DrawableRes int)
设置页面间隔的显示图,显然,必须用setPageMargin方法设置了间隔距离之后本方法才有意义,否则0间隔肯定显示不出来。
关于PagerAdapter
这是一个抽象类,每一个ViewPager都依赖于PagerAdapter的实现来填充相应的页面(除非你的ViewPager空空如也,不显示任何内容,但那有什么意义呢?)。
要实现PagerAdapter,至少需要覆盖4个方法:
- public abstract int getCount()
很好理解,确定ViewPager一共有多少个页面。
- public abstract boolean isViewFromObject(View, Object)
不太容易理解,文档的说法是,这个方法用来确定页面View是否与instantiateItem方法返回的key对象相关联,PagerAdapter要正常运行就必须实现这个方法,文档还说,覆盖这个方法只需要填写
return view == object;
即可。显然,文档说的有些模糊,大家很难理解这个方法的意义所在。
有必要去看一下ViewPager的源码了。在ViewPager里,有这样一个方法:
ViewPager.ItemInfo infoForChild(View child) { for(int i = 0; i < this.mItems.size(); ++i) { ViewPager.ItemInfo ii = (ViewPager.ItemInfo)this.mItems.get(i); if (this.mAdapter.isViewFromObject(child, ii.object)) { return ii; } } return null; }
它调用了isViewFromObject方法,继续在源码里找,就知道了是怎么回事:ViewPager本质上是一个ViewGroup,每个页面都算是这个ViewGroup的child。众多的child是如何管理的呢?它有一个静态内部类:
static class ItemInfo { Object object; int position; boolean scrolling; float widthFactor; float offset; ItemInfo() { } }
然后有这样一个List:
private final ArrayList<ViewPager.ItemInfo> mItems = new ArrayList();
实际上mItems就存放了ViewPager里的所有页面,而每个页面的View实际上就是ItemInfo的object字段,因为该字段是Object的,所以无论是一个简单的布局还是复杂的布局,都没问题。
而ViewPager是如何确定管理所有页面的?通过infoForChild方法的实现能看到,实际上ViewPager把object字段本身当成了一个key,也就是说,ViewPager的每个页面的key就是它自己本身这个View对象了……
而instantiateItem方法返回的Object最终会生成一个ItemInfo对象,添加到mItems中。而isViewFromObject 方法其实就是在判断,第一个参数view到底跟object是不是一个对象。
感觉这一段说的还是有点绕,反正大意能看明白就好,感觉Google对这一部分实现的不是很好……
- public Object instantiateItem(ViewGroup, int)
用来创建给定位置的页面,适配器会把创建的View添加到给定的容器container中。
- public void destroyItem(ViewGroup, int, Object)
用来移除给定位置的页面,适配器会把View从容器里删除。
刚才说过,这4个方法是必须覆盖的,但是可以看到,4个方法里只有getCount()和isViewFromObject(View var1, Object var2)是抽象方法,是强制你实现的,那么另外2个呢?实际上我们可以尝试下,自己实现一个PagerAdapter,只覆盖2个抽象方法,结果如何呢?
java.lang.UnsupportedOperationException: Required method instantiateItem was not overridden
java.lang.UnsupportedOperationException: Required method destroyItem was not overridden
编译自然是能通过的,但是运行的时候直接崩溃,得到了UnsupportedOperationException异常。其实不难理解,没instantiateItem这个方法,ViewPager根本就无法显示页面,所以肯定是需要的。而destroyItem负责移除,当ViewPager滑动的时候,距离当前页面位置较远的页面是没必要继续保持的,移除掉能节约内存,对性能表现有帮助。
不过,这2个方法如此重要,重要到没有就崩的程度,却没有设计成abstract修饰的抽象方法,我个人实在是不太懂Android官方开发人员的思维。
ViewPager的使用
- ViewPager的初始化
先在布局文件里添加一个ViewPager:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.view.ViewPager android:id="@+id/vpTest" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.constraint.ConstraintLayout>
然后在代码里(如Activity或Fragment)通过findViewById的方法进行初始化:
private ViewPager vpTest; vpTest = findViewById(R.id.vpTest);
- 实现自己的PagerAdapter:
首先我把数据准备好:
private List<String> mDataList = Arrays.asList("测试数据", "ViewPager的使用", "PagerAdapter的实现", "这是最后一个页面");
然后写了一个LayoutParams对象,待会有用:
private ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
然后是PagerAdapter的实现:
private class TestAdapter extends PagerAdapter { @Override public int getCount() { return mDataList.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { return view == o; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { TextView mTextView = new TextView(VPActivity.this); mTextView.setText(mDataList.get(position)); mTextView.setTextColor(Color.RED); mTextView.setTextSize(24); mTextView.setGravity(Gravity.CENTER); mTextView.setLayoutParams(lp); container.addView(mTextView); return mTextView; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((TextView)object); } }
不难看出,此次ViewPager的每个子视图是一个简单的TextView,而TextView的文字则从mDataList中按照位置获取。
接下来,就是通过setAdapter方法,把ViewPager和PagerAdapter进行绑定:
vpTest.setAdapter(new TestAdapter());
然后我们来看一下运行效果:
评论