ViewPager与适配器PagerAdapter搭配使用详解与源码解析

KaelLi 2019年2月15日18:11:08
评论
14,3152

什么是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());

然后我们来看一下运行效果:

ViewPager与适配器PagerAdapter搭配使用详解与源码解析

KaelLi
  • 本文由 发表于 2019年2月15日18:11:08
  • 转载请务必保留本文链接:https://www.kaelli.com/30.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: