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()方法存在三個坑,看了下好像的確有些道理,大家可以自行了解一下。http://www.lxweimin.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閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容