Android - ViewPager 从基础到进阶

前言

好记性不如烂笔头,学习的知识总要记录下来,通过本文来加深对 ViewPager 方方面面的理解:

  • ViewPager 的基础介绍
  • PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
  • 与 Fragment + TabLayout 的联动使用
  • Banner 轮播图
  • 自定义切换动画
  • 首次登录引导界面

闲话少说,下面进入正题。

基础介绍

ViewPager 是Android support v4 包中的类,官方文档对其描述如下:

Layout manager that allows the user to flip left and right through pages of data.

意思是说,其本身是一个布局管理器,允许我们左右滑动来切换不同的数据页面。

它直接继承自 ViewGroup 类,说明它是一个容器类,可以在其中添加其他View,实际上我们也就是这么用的。

在使用时,直接在布局中加入 ViewPager 即可,相信大家都会,至于其中的属性,就只有一个 android:clipChildren 需要注意一下,我们后面会说,其他都和一般的 ViewGroup 没什么区别(其实这个clipChildren属性也是源自 ViewGroup 的~)。

这里提一下几个动态设置方法,能不能实现 漂亮花哨的效果,基本就靠这几个方法:

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

同时它需要实现一个 PagerAdapter 适配器,和 ListView,RecyclerView 类似,适配器用来提供数据,填充页面。

ViewPager 适配器 - PagerAdapter

PagerAdapter 是一个抽象类,因此我们只能使用它的实现类,官方为我们提供了两个直接子类 FragmentPagerAdapter 和 FragmentStatePagerAdapter ,基本都是ViewPager + Fragment 搭配时使用的。

但是,我们使用ViewPager显然不是只为了和 Fragment 打交道的,比如实现后面会讲到的轮播图,因此我们仍要按需实现合适的适配器,现在先看看如何去实现一个PagerAdapter子类,主要就是以下4个方法(必须实现):

  • int getCount():获取页面数。
  • boolean isViewFromObject(View view, Object object):判断页面视图是否和instantiateItem()方法返回的对象相关联,总之通常直接返回 return view == object;
  • Object instantiateItem(View container, int position):作用是对要显示或缓存的界面,进行布局的初始化。
  • void destroyItem(ViewGroup container, int position, Object object): 销毁页面。

我们来看一下源码中对 ViewPager执行流程的解释,来加深理解。

ViewPager associates each page with a key Object instead of working with Views directly. This key is used to track and uniquely identify a given page independent of its position in the adapter.

ViewPager 并不是直接处理视图,而是将每个页面与一个key Object(没错就是instantiateItem()返回的东西)关联起来,这个 key Object 跟踪并且唯一标识一个给定的页面。

A very simple PagerAdapter may choose to use the page Views themselves as key objects, returning them from {@link #instantiateItem(ViewGroup, int)} after creation and adding them to the parent ViewGroup. A matching {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the View from the parent ViewGroup and {@link #isViewFromObject(View, Object)} could be implemented as return view == object;.

最通常的PagerAdapter实现(也就是只实现上面的4个方法),是将页面视图本身作为key Object,在创建后通过instantiateItem()方法返回,并将它们添加到父容器ViewGroup 中,当我们不需要某视图或者缓存达到上限时,destroyItem()方法被调用,会将该视图从父ViewGroup中移除。最后Google建议我们直接在isViewFromObject()方法中直接返回return view == object;

更多关于ViewPager的处理逻辑,建议直接看源码中的注释,涉及到其他的各种方法,此处就不再多说了。

ViewPager + TabLayout + Fragment

理论

Google 官方文档中 Creating swipe views with tabs 这一节中,介绍的是 ViewPgaer + Fragment + Action bar tabs/ PagerTitleStrip 实现导航页,三者联动使用。但是随着 Material Design 中 TabLayout 的推出,直接秒杀上述tabs或PagerTitleStrip(其实从效果上来看差不太多,但是 TabLayout 可以一行代码外加一个方法搞定和ViewPager 的联动,比前者方便太多),所以本文就直接介绍和 TabLayout 的配合使用。

前面我们也提到了官方为我们提供了两个PagerAdapter的直接子类:FragmentPagerAdapter和FragmentStatePagerAdapter,不知道在座的读者你们是什么感觉,我是觉得很奇怪,为什么针对 Fragment 要搞两个子类出来?

这种时候,看看源码就清楚了,主要区别主要在destroyItem()方法:

//FragmentPagerAdapter.java
@Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        mCurTransaction.detach((Fragment)object);
    }

//FragmentStatePagerAdapter.java
@Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

        mCurTransaction.remove(fragment);
    }

源码解释的很清楚,FragmentPagerAdapter 只是将 销毁视图,而不是销毁Fragment 实例,而FragmentStatePagerAdapter 则是彻底将 Fragment 从当前的 FragmentManager中溢出,但是会保存 Fragment 的状态信息(也就是名字中State的意义),等到需要重建(切换回该页面)时,通过状态信息进行恢复创建。

官方(源码)建议我们使用这二者的场景如下:

FragmentPagerAdapter:适合用于展示静态的fragment,主页面等,类似几个tabs。此时,不会占有太大的内存,同时避免因反复销毁创建浪费时间。

FragmentStatePagerAdapter:类似ListView,需要展示大量页面时,由于大量页面对用户不可见,当Fragment被销毁时,我们只会保存其状态信息,这样会节省大量的内存。

emmm...好像说的有点远了,下面介绍如何使用。

二者从使用上来看是毫无区别的,实现两个方法:

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

嗯,下面就到如何实现 TabLayout 和 ViewPager 的联动了,等我下面介绍完,我相信你会惊讶于它怎么会如此简单的,只要两个步骤:

  1. 初始化后调用 TabLayout.setupWithViewPager(ViewPager)方法,将二者绑定到一起。
  2. 重写 PagerAdapter 的 public CharSequence getPageTitle(int position) 方法。 TabLayout 会通过 setupWithViewPager() 方法底部会调用 PagerAdapter 中的getPageTitle() 方法来获取 title 并更新自己的 tab 的。

在网上看到一篇文章说到setupWithViewPager()方法存在三个坑,看了下好像的确有些道理,大家可以自行了解一下。https://www.jianshu.com/p/896b149aaa43

理论知识暂时告一段落,下面进入实践时间

实例

先放上最终的效果图:(顶部绿色导航基于 TabLayout 实现,而下方的蓝(青?)色的是 PagerTitleStrip 的默认效果,只是为了凸显二者的区别)

image

嗯。。还是挺简单的,直接上代码吧:

TabActivity.java

public class TabActivity extends AppCompatActivity {
    private ViewPager mViewPager;
    private TabLayout mTabLayout;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tabfragment);

        mViewPager = findViewById(R.id.view_pager_tab);
        mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
            private String[] titles = new String[]{"Deemo", "Cytus", "兰空", "万向物语", "绝地求生", "魔女之泉"};

            @Override
            public Fragment getItem(int position) {
                return PageFragment.newinstance(position);
            }

            @Override
            public int getCount() {
                return titles.length;
            }

            @Nullable
            @Override
            public CharSequence getPageTitle(int position) {
                return titles[position];
            }
        });
        mTabLayout = findViewById(R.id.tablayout);
        mTabLayout.setupWithViewPager(mViewPager);

        //设置标签摆放方式
        //默认为MODE_FIXED,固定模式
        //mTabLayout.setTabMode(TabLayout.MODE_FIXED);

        //滑动模式
        mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
    }
}

PageFragment.java

public class PageFragment extends Fragment {
    public static final String ARGS = "PageFragment";

    private int curPage;

    public static PageFragment newinstance(int curPage) {
        Bundle args = new Bundle();
        args.putInt(ARGS, curPage);
        PageFragment fragment = new PageFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        curPage = getArguments().getInt(ARGS);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_page, container, false);
        TextView textView = view.findViewById(R.id.text_view);
        textView.setText("Page :" + curPage);
        return view;
    }
}

activity_tabfragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.TabLayout
        android:id="@+id/tablayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ffaa"/>
    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager_tab"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
        <android.support.v4.view.PagerTitleStrip
            android:id="@+id/pager_title_strip"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:background="#33b5e5"
            android:textColor="#fff"
            android:paddingTop="4dp"
            android:paddingBottom="4dp" />
    </android.support.v4.view.ViewPager>

</LinearLayout>

fragment_page.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:gravity="center" />

</LinearLayout>

这个组合还是挺常用的,尤其是MD风格的APP中尤为常见,建议还是要能够熟练使用(虽然我才入坑不久,但是菜就不能提建议了么~)

ViewPager 轮播

首先盗个图~

从构成元素来讲,就这么几个:标题&指示器、切换动画、自动轮播、首位循环无限轮播。(页面本身用一个 ImageView 填充,应该不需要在额外强调什么吧~)

标题&指示器

比较常见的写法是在ViewPager所在布局中,声明指示器和标题布局:

acctivity_banner.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="160dp"
    android:layout_centerInParent="true"
    android:background="#1be2be">

    <android.support.v4.view.ViewPager
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/view_pager"
        android:layout_gravity="center"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:orientation="vertical">
        <LinearLayout
            android:id="@+id/indicator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:orientation="horizontal" />
        <TextView
            android:id="@+id/banner_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#7d868585"
            android:text="I'm whdalive, an handsome man"/>
    </LinearLayout>


</FrameLayout>

可能有童鞋要问:为什么不直接把 标题和指示器 放到 Banner 的 Item 里面呢,这样我们只要复写 instantiateItem() 不就可以直接完成初始化了?嗯,关于这点,只是为了切换效果好一点,仅此而已,没有什么额外的用意。

然后需要注意我们上面 小圆点 指示器使用了一个 LinearLayout,这是因为某些情况下,我们预先可能不知道会有多少个页面,所以我们干脆直接用一个 LinearLayout,在代码中动态加载指示器的 view 添加进来。

现在我们有了标题和指示器,下面就要考虑如何让这二者与页面联动了。

这就用到了 addOnPageChangeListener()这个方法,该方法会设置一个OnPageChangeListener监听器,用来监听页面的变化。其中有三个回调方法:

  1. onPageScrolled():当前页面发生滑动时调用
  2. onPageSelected():页面滑动结束,选定页面时调用。需要注意的是,该方法调用时,动画未必完成
  3. onPageScrollStateChanged():当滑动状态改变时调用,即处理何时开始滑动,或何时滑动停止。

于是乎,我们只需要回调onPageSelected()方法即可,在此方法中设置标题和指示器跟随变化即可。

实例如下:

mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        //处理指示器(小圆点)的显示逻辑
        for (int i = 0; i < dotsList.size(); i++) {
            if (position % dotsList.size() == i) {
                dotsList.get(i).setImageResource(R.drawable.indicator_focus);

            } else {
                dotsList.get(i).setImageResource(R.drawable.indicator_normal);
            }
        }
        //设置标题
        bannerTitle.setText(titles[position]);
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }
});

关于页面本身的加载,就只是用一个 ArrayList<ImageView> 来存 Banner 的图片资源,当然为了顺畅运行,我是使用了 Glide 加载图片(直接调用imageView.setImageResource(R.drawable.XXXX);时模拟器卡的动不了,主要还是图片资源太大了。= =),以下是实现 PagerAdapter 子类填充页面的部分代码。

mViewPager.setAdapter(new PagerAdapter() {
    @Override
    public int getCount() {
        return imgs.length;
    }

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

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

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

切换动画

切换动画,主要是用到setPageTransformer(boolean .. ,PageTransformer ...)方法来设置动画,该方法会接收一个 PageTransfromer 参数,这就是动画的核心关键所在了。

PageTransformer 实际上是一个接口,内部只有一个方法 void transformPage(@NonNull View page, float position);,该方法接收两个参数,一个 View 显然就是我们的页面了,当然这个 页面 涵盖了当前显示的页面、即将滑出的页面、即将滑入的页面以及隐藏的页面,而这么多页面,如何区分呢?这就第二个参数 position 的作用了。首先,千万不要和 ViewPager 下标的 position 混淆了(float 类型你告诉我是下标?),源码中对 position 的解释如下:

View 的 position 和 ViewPager 当前的中心位置有关,当前选中的页面 position 是 0,前一个页面是 -1,后一个页面是 1。

但是有同学指出:

前后 item position 为 -1 和 1 的前提是你没有给 ViewPager 设置 pageMargin。如果你设置了 pageMargin,前后 item 的 position 需要分别加上(或减去,前减后加)一个偏移量(偏移量的计算方式为 pageMargin / pageWidth)。

嗯,然后当我们页面滑动的时候,position 是动态变化的,transformPage()会根据 position 的值来对页面进行属性变换,position 的变化规律如下:(不考虑pageMargin,方便讲解)

  1. position 分为三段:(-∞,-1)[-1,1](1,∞)
  2. 对于左右两个,多数时是不可见的,因此只需要分析以下[-1,1]区间
  3. 以第一页->第二页(左滑)为例:
    1. 页1的position:0->-1
    2. 页1的position:1->0
  4. 根据上述,我们就可以通过setAlpha()等方法设置属性,以此达到自定义切换动画的效果。(实际和属性动画有那么一点点类似)

实例嘛,见这节结束的实例就好了,此处不多搞了。

切换动画,可塑性实在是太高了,基本只有你想不到,没有它做不到的,于是后面我们会再扩充几种切换动画来加深理解。

自动轮播

自动轮播,听起来高大上,原理简单的离谱:每隔一定时间给它一个事件,告诉它“嘿,你该切换页面了”。嗯,说到这,不就是调用Handler.sendEmptyMessageDelayed(int what, long delayMillis)的小事了么~

Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        //mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//当实现首尾循环无限轮播时的第一种方案时会这么设置,后面再说。
        mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mList.size());
        this.sendEmptyMessageDelayed(MSG_WHAT, 2000);
    }
};

有了上述代码,我们只需要在初始化 ViewPager 之后调用依次Handler.sendEmptyMessageDelayed(int what, long delayMillis)就ok了。

当然实践中,我们可能需要对自动轮播进一步处理,譬如判断滑动手势暂停轮播,我们总不会希望“我错过了一个感兴趣的广告,然后把页面滑动回去,结果很快页面又!自动滑动回来了”,这种体验估计就很差。我在此处就不加以实现了,大家可以自行尝试一下,毕竟我只是讲解向~~(其实只是手势判断还没接触 ~~)。

首尾循环无限轮播

关于首尾无限轮播,指的是在第一个页面时向左滑动能够连贯的滑动到最后一页,而在最后一页向右滑动时,能顺畅的滑动到第一页。

起初我是没有注意到有什么坑的,但是当我按照上面的代码运行之后,发现首尾十分的不连贯,会连续滑过中间的所有页面,显然并不能满足我们的需求。

对于首尾循环的轮播,我也是参考网上的思路,就简单介绍一下:

  1. 设置 ViewPager 展示的个数为Inreger.MAX_VALUE,初始化时,将当前页面设置为n*mList.size(),除非闲得蛋疼,不然没什么人有毅力滑个Integer.MAX_VALUE次吧,所以说通常是没什么问题的。
  2. 在首尾分别加入最后一页和当前一页,比如 原来是 a,b,c 现在变为 c,a,b,c,a,当从末尾的c滑动到a时,将页面切换为第一个a。同理在第一个a左滑动到c时,将页面切换到第二个c。缺点可能就时可能会有短暂的延时?

贴出来参考的文章

https://blog.csdn.net/zhiyuan0932/article/details/52673169

https://blog.csdn.net/anyfive/article/details/52525262

实例

效果图呈上:

这里写图片描述

BannerActivity.java

public class BannerActivity extends AppCompatActivity {

    private static final int MSG_WHAT = 0;

    private int[] imgs;
    private ViewPager mViewPager;
    private List<ImageView> mList = new ArrayList<>();
    private String[] titles;
    Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//无限轮播时
            mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mList.size());
            this.sendEmptyMessageDelayed(MSG_WHAT, 2000);
        }
    };
    private LinearLayout mLinearLayout;
    private ArrayList<ImageView> dotsList;

    private TextView bannerTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_banner);
        imgs = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.e, R.drawable.f};
        titles = new String[]{"To think as great minds, to do as idiots","One Step Closer To The Hell","Knowing Everything of Something","Nothing For Nothing","No Royal Road To Anything"};
        bannerTitle = findViewById(R.id.banner_title);
        mLinearLayout = findViewById(R.id.indicator);
        init();
        initDots();

        mViewPager = findViewById(R.id.view_pager);
        mViewPager.setOffscreenPageLimit(3);//设置缓存页面数量

        mViewPager.setPageTransformer(true, new BannerPageTransformer());


        mViewPager.setAdapter(new PagerAdapter() {
            @Override
            public int getCount() {
                return imgs.length;
            }

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

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

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

        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                for (int i = 0; i < dotsList.size(); i++) {
                    if (position % dotsList.size() == i) {
                        dotsList.get(i).setImageResource(R.drawable.indicator_focus);

                    } else {
                        dotsList.get(i).setImageResource(R.drawable.indicator_normal);
                    }
                }
                bannerTitle.setText(titles[position]);
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });

        mHandler.sendEmptyMessageDelayed(MSG_WHAT, 2000);
    }

    private void init() {
        for (int img : imgs) {
            ImageView imageView = new ImageView(getApplicationContext());
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            //imageView.setImageResource(imgid);
            Glide.with(getApplicationContext()).load(img).into(imageView);
            mList.add(imageView);
        }
    }

    private void initDots() {
        dotsList = new ArrayList<>();
        for (int i = 0; i < imgs.length; i++) {
            ImageView imageView = new ImageView(getApplicationContext());
            if (i == 0) {
                imageView.setImageResource(R.drawable.indicator_focus);
            } else {
                imageView.setImageResource(R.drawable.indicator_normal);
            }
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(16, 16);

            params.setMargins(5, 0, 5, 0);
            mLinearLayout.addView(imageView, params);
            dotsList.add(imageView);
        }
    }
}

BannerPageTransformer.java

public class BannerPageTransformer implements ViewPager.PageTransformer {
    @Override
    public void transformPage(@NonNull View page, float position) {
        int width = page.getWidth();

        if (position < -1) {
            page.setScrollX((int) (width * 0.75 * -1));
        } else if (position <= 1) {
            page.setScrollX((int) (width * 0.75 * position));
        } else {
            page.setScrollX((int) (width * 0.75));
        }
    }
}

activity_banner.xml

见前几节。

ViewPager 切换动画扩充

ZoomOutPageTransformer

这里写图片描述

RotateDownPageTransformer

这里写图片描述

注意,为了再ViewPager中可以同时显示多个页面,我们需要再布局中 设置 ViewPager 及其父容器的 clipChildren 属性为 false。

activity_trans.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="160dp"
    android:clipChildren="false"
    android:layout_centerInParent="true"
    android:background="#1be2be">

    <android.support.v4.view.ViewPager
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:id="@+id/view_pager_trans"
        android:layout_marginLeft="60dp"
        android:layout_marginRight="60dp"
        android:layout_gravity="center"
        android:clipChildren="false"/>

</LinearLayout>

嗯,其他的好像没什么可说得了(毕竟我的这两个切换效果一个是摘自Google官方,一个摘自 鸿洋 大佬。。),就推荐一个两个开源库吧

  1. GitHub上比较火的广告轮播控件,虽然是几年前的东西,但还是很值得参考的:AndroidImageSlider
  2. 一个看起来还不错的切换效果合辑 PageTransformerHelp

另外,给出 鸿洋 大佬关于自定义切换效果的文章,大佬的文章还是很值得学习的。

巧用ViewPager 打造不一样的广告轮播切换效果

View Pager + Fragment + SharedPreferences 首次登录引导界面

还是先将效果图放出来吧(图片和上面相同的资源,毕竟只是讲解思路嘛~ 丑点就丑点吧~)
(为了图省事,直接从CSDN把图扒过来,然后又图省事,在线压缩gif,结果就来了两重水印。。蛋碎了一地。)

实际上和上面也没有什么本质上的区别,所以在此就只介绍一下思路吧。

只是利用 SharedPreferences 来记录当前是否为第一次登录,指示器和上述实现一致,同时加入两个按钮,右上角 skip(始终存在),指示器上方 got it(当滑动到最后一页时出现),二者点击时都会启动主页面。

除此之外,该模式可以有很多变型:

  • 右上角 skip 倒计时,倒计时完成后自动启动主页面,也可点击进入主页面
  • 左右滑动的页面可以设置为 自动轮播,播放到最后一页时 自动进入主页面
  • 不给 skip ,强制观看完所有引导页之后,才能通过弹出的got it 进入主页面
  • …………

代码如下:(其实你会发现,代码和上面的代码 差别很小~)

WelcomeActivity.java

public class WelcomeActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private AppCompatButton btn_got;
    private AppCompatButton btn_skip;
    private LinearLayout mLinearLayout;
    private ArrayList<ImageView> dotsList;

    private int[] imgs;
    private List<ImageView> mList = new ArrayList<>();

    private SharedPreferences mPreferences;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        imgs = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.e, R.drawable.f};
        if (mPreferences.getBoolean("FirstLaunch", true)) {
            setContentView(R.layout.activity_welcome);
            mLinearLayout = findViewById(R.id.indicator_welcome);
            initView();
            initDots();
            mViewPager.setAdapter(new PagerAdapter() {
                @Override
                public int getCount() {
                    return imgs.length;
                }

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

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

                @Override
                public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
                    container.removeView(mList.get(position));
                }
            });
            mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                }

                @Override
                public void onPageSelected(int position) {
                    for (int i = 0; i < dotsList.size(); i++) {
                        if (position % dotsList.size() == i) {
                            dotsList.get(i).setImageResource(R.drawable.indicator_focus);
                        } else {
                            dotsList.get(i).setImageResource(R.drawable.indicator_normal);
                        }
                    }
                    btn_got.setVisibility(position == mList.size()-1?View.VISIBLE:View.GONE);
                }

                @Override
                public void onPageScrollStateChanged(int state) {

                }
            });
            btn_got.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    recordFirstLaunch();
                    notFirstLaunch();
                }
            });
            btn_skip.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    recordFirstLaunch();
                    notFirstLaunch();
                }
            });

        } else {
            notFirstLaunch();
            finish();
        }
    }

    private void recordFirstLaunch() {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putBoolean("FirstLaunch", false);
        editor.apply();
        notFirstLaunch();
    }

    private void notFirstLaunch() {
        startActivity(new Intent(this, MainActivity.class));
    }

    private void initView() {
        mViewPager = findViewById(R.id.view_pager);
        btn_got = findViewById(R.id.btn_got);
        btn_skip = findViewById(R.id.skip);
        for (int img : imgs) {
            ImageView imageView = new ImageView(getApplicationContext());
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            //imageView.setImageResource(imgid);
            Glide.with(getApplicationContext()).load(img).into(imageView);
            mList.add(imageView);
        }
    }

    private void initDots() {
        dotsList = new ArrayList<>();
        for (int i = 0; i < imgs.length; i++) {
            ImageView imageView = new ImageView(getApplicationContext());
            if (i == 0) {
                imageView.setImageResource(R.drawable.indicator_focus);
            } else {
                imageView.setImageResource(R.drawable.indicator_normal);
            }
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(16, 16);

            params.setMargins(5, 0, 5, 0);
            mLinearLayout.addView(imageView, params);
            dotsList.add(imageView);
        }
    }
}

activity_welcome.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.v7.widget.AppCompatButton
        android:id="@+id/skip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="skip"
        android:textAllCaps="false"
        android:layout_gravity="top|end"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <android.support.v7.widget.AppCompatButton
        android:id="@+id/btn_got"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Got it"
        android:layout_gravity="bottom|center"
        android:layout_marginBottom="16dp"
        android:visibility="gone"/>

    <LinearLayout
        android:id="@+id/indicator_welcome"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:gravity="center_horizontal"
        android:orientation="horizontal">
    </LinearLayout>


</android.support.design.widget.CoordinatorLayout>

总结

本文针对 ViewPager 尽可能的介绍各种使用方法,涵盖如下:

  • 基础介绍
  • PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
  • 与 Fragment + TabLayout 的联动使用
  • Banner 轮播图
  • 自定义切换动画
  • 首次登录引导界面

放上源码地址,可以下载下来配合学习。

源码地址https://github.com/whdalive/Demo-ViewPager

洋洋洒洒写了这么多,最后愿本文对大家有所帮助。互勉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容