美文网首页
Android - ViewPager 从基础到进阶

Android - ViewPager 从基础到进阶

作者: whd_Alive | 来源:发表于2018-06-04 21:13 被阅读0次

    前言

    好记性不如烂笔头,学习的知识总要记录下来,通过本文来加深对 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

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

    相关文章

      网友评论

          本文标题:Android - ViewPager 从基础到进阶

          本文链接:https://www.haomeiwen.com/subject/svfusftx.html