Android - ViewPager 從基礎(chǔ)到進(jìn)階

前言

好記性不如爛筆頭,學(xué)習(xí)的知識(shí)總要記錄下來,通過本文來加深對(duì) ViewPager 方方面面的理解:

  • ViewPager 的基礎(chǔ)介紹
  • PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
  • 與 Fragment + TabLayout 的聯(lián)動(dòng)使用
  • Banner 輪播圖
  • 自定義切換動(dòng)畫
  • 首次登錄引導(dǎo)界面

閑話少說,下面進(jìn)入正題。

基礎(chǔ)介紹

ViewPager 是Android support v4 包中的類,官方文檔對(duì)其描述如下:

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

意思是說,其本身是一個(gè)布局管理器,允許我們左右滑動(dòng)來切換不同的數(shù)據(jù)頁面。

它直接繼承自 ViewGroup 類,說明它是一個(gè)容器類,可以在其中添加其他View,實(shí)際上我們也就是這么用的。

在使用時(shí),直接在布局中加入 ViewPager 即可,相信大家都會(huì),至于其中的屬性,就只有一個(gè) android:clipChildren 需要注意一下,我們后面會(huì)說,其他都和一般的 ViewGroup 沒什么區(qū)別(其實(shí)這個(gè)clipChildren屬性也是源自 ViewGroup 的~)。

這里提一下幾個(gè)動(dòng)態(tài)設(shè)置方法,能不能實(shí)現(xiàn) 漂亮花哨的效果,基本就靠這幾個(gè)方法:

  • setAdapter(PagerAdapter adapter) 設(shè)置適配器
  • setOffscreenPageLimit(int limit) 設(shè)置緩存的頁面?zhèn)€數(shù),默認(rèn)是 1
  • setCurrentItem(int item) 跳轉(zhuǎn)到特定的頁面
  • addOnPageChangeListener(..) 設(shè)置頁面滑動(dòng)時(shí)的監(jiān)聽器
  • setPageTransformer(..PageTransformer) 設(shè)置頁面切換時(shí)的動(dòng)畫效果
  • setPageMargin(int marginPixels) 設(shè)置不同頁面之間的間隔
  • setPageMarginDrawable(..) 設(shè)置不同頁面間隔之間的裝飾圖也就是 divide ,要想顯示設(shè)置的圖片,需要同時(shí)設(shè)置 setPageMargin()

同時(shí)它需要實(shí)現(xiàn)一個(gè) PagerAdapter 適配器,和 ListView,RecyclerView 類似,適配器用來提供數(shù)據(jù),填充頁面。

ViewPager 適配器 - PagerAdapter

PagerAdapter 是一個(gè)抽象類,因此我們只能使用它的實(shí)現(xiàn)類,官方為我們提供了兩個(gè)直接子類 FragmentPagerAdapter 和 FragmentStatePagerAdapter ,基本都是ViewPager + Fragment 搭配時(shí)使用的。

但是,我們使用ViewPager顯然不是只為了和 Fragment 打交道的,比如實(shí)現(xiàn)后面會(huì)講到的輪播圖,因此我們?nèi)砸葱鑼?shí)現(xiàn)合適的適配器,現(xiàn)在先看看如何去實(shí)現(xiàn)一個(gè)PagerAdapter子類,主要就是以下4個(gè)方法(必須實(shí)現(xiàn)):

  • int getCount():獲取頁面數(shù)。
  • boolean isViewFromObject(View view, Object object):判斷頁面視圖是否和instantiateItem()方法返回的對(duì)象相關(guān)聯(lián),總之通常直接返回 return view == object;
  • Object instantiateItem(View container, int position):作用是對(duì)要顯示或緩存的界面,進(jìn)行布局的初始化。
  • void destroyItem(ViewGroup container, int position, Object object): 銷毀頁面。

我們來看一下源碼中對(duì) ViewPager執(zhí)行流程的解釋,來加深理解。

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 并不是直接處理視圖,而是將每個(gè)頁面與一個(gè)key Object(沒錯(cuò)就是instantiateItem()返回的東西)關(guān)聯(lián)起來,這個(gè) key Object 跟蹤并且唯一標(biāo)識(shí)一個(gè)給定的頁面。

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實(shí)現(xiàn)(也就是只實(shí)現(xiàn)上面的4個(gè)方法),是將頁面視圖本身作為key Object,在創(chuàng)建后通過instantiateItem()方法返回,并將它們添加到父容器ViewGroup 中,當(dāng)我們不需要某視圖或者緩存達(dá)到上限時(shí),destroyItem()方法被調(diào)用,會(huì)將該視圖從父ViewGroup中移除。最后Google建議我們直接在isViewFromObject()方法中直接返回return view == object;

更多關(guān)于ViewPager的處理邏輯,建議直接看源碼中的注釋,涉及到其他的各種方法,此處就不再多說了。

ViewPager + TabLayout + Fragment

理論

Google 官方文檔中 Creating swipe views with tabs 這一節(jié)中,介紹的是 ViewPgaer + Fragment + Action bar tabs/ PagerTitleStrip 實(shí)現(xiàn)導(dǎo)航頁,三者聯(lián)動(dòng)使用。但是隨著 Material Design 中 TabLayout 的推出,直接秒殺上述tabs或PagerTitleStrip(其實(shí)從效果上來看差不太多,但是 TabLayout 可以一行代碼外加一個(gè)方法搞定和ViewPager 的聯(lián)動(dòng),比前者方便太多),所以本文就直接介紹和 TabLayout 的配合使用。

前面我們也提到了官方為我們提供了兩個(gè)PagerAdapter的直接子類:FragmentPagerAdapter和FragmentStatePagerAdapter,不知道在座的讀者你們是什么感覺,我是覺得很奇怪,為什么針對(duì) Fragment 要搞兩個(gè)子類出來?

這種時(shí)候,看看源碼就清楚了,主要區(qū)別主要在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);
    }

源碼解釋的很清楚,F(xiàn)ragmentPagerAdapter 只是將 銷毀視圖,而不是銷毀Fragment 實(shí)例,而FragmentStatePagerAdapter 則是徹底將 Fragment 從當(dāng)前的 FragmentManager中溢出,但是會(huì)保存 Fragment 的狀態(tài)信息(也就是名字中State的意義),等到需要重建(切換回該頁面)時(shí),通過狀態(tài)信息進(jìn)行恢復(fù)創(chuàng)建。

官方(源碼)建議我們使用這二者的場景如下:

FragmentPagerAdapter:適合用于展示靜態(tài)的fragment,主頁面等,類似幾個(gè)tabs。此時(shí),不會(huì)占有太大的內(nèi)存,同時(shí)避免因反復(fù)銷毀創(chuàng)建浪費(fèi)時(shí)間。

FragmentStatePagerAdapter:類似ListView,需要展示大量頁面時(shí),由于大量頁面對(duì)用戶不可見,當(dāng)Fragment被銷毀時(shí),我們只會(huì)保存其狀態(tài)信息,這樣會(huì)節(jié)省大量的內(nèi)存。

emmm...好像說的有點(diǎn)遠(yuǎn)了,下面介紹如何使用。

二者從使用上來看是毫無區(qū)別的,實(shí)現(xiàn)兩個(gè)方法:

  • public Fragment getItem(int position) 返回對(duì)應(yīng) Fragment 實(shí)例,一般我們?cè)谑褂脮r(shí),會(huì)通過構(gòu)造傳入一個(gè)要顯示的 Fragment 的集合,我們只要在這里把對(duì)應(yīng)的 Fragment 返回就行了
  • public int getCount() 返回的是頁面的個(gè)數(shù),我們只要返回傳入 Fragment 集合的長度就行了。

嗯,下面就到如何實(shí)現(xiàn) TabLayout 和 ViewPager 的聯(lián)動(dòng)了,等我下面介紹完,我相信你會(huì)驚訝于它怎么會(huì)如此簡單的,只要兩個(gè)步驟:

  1. 初始化后調(diào)用 TabLayout.setupWithViewPager(ViewPager)方法,將二者綁定到一起。
  2. 重寫 PagerAdapter 的 public CharSequence getPageTitle(int position) 方法。 TabLayout 會(huì)通過 setupWithViewPager() 方法底部會(huì)調(diào)用 PagerAdapter 中的getPageTitle() 方法來獲取 title 并更新自己的 tab 的。

在網(wǎng)上看到一篇文章說到setupWithViewPager()方法存在三個(gè)坑,看了下好像的確有些道理,大家可以自行了解一下。http://www.itdecent.cn/p/896b149aaa43

理論知識(shí)暫時(shí)告一段落,下面進(jìn)入實(shí)踐時(shí)間

實(shí)例

先放上最終的效果圖:(頂部綠色導(dǎo)航基于 TabLayout 實(shí)現(xiàn),而下方的藍(lán)(青?)色的是 PagerTitleStrip 的默認(rèn)效果,只是為了凸顯二者的區(qū)別)

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);

        //設(shè)置標(biāo)簽擺放方式
        //默認(rèn)為MODE_FIXED,固定模式
        //mTabLayout.setTabMode(TabLayout.MODE_FIXED);

        //滑動(dòng)模式
        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>

這個(gè)組合還是挺常用的,尤其是MD風(fēng)格的APP中尤為常見,建議還是要能夠熟練使用(雖然我才入坑不久,但是菜就不能提建議了么~)

ViewPager 輪播

首先盜個(gè)圖~

從構(gòu)成元素來講,就這么幾個(gè):標(biāo)題&指示器、切換動(dòng)畫、自動(dòng)輪播、首位循環(huán)無限輪播。(頁面本身用一個(gè) ImageView 填充,應(yīng)該不需要在額外強(qiáng)調(diào)什么吧~)

標(biāo)題&指示器

比較常見的寫法是在ViewPager所在布局中,聲明指示器和標(biāo)題布局:

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>

可能有童鞋要問:為什么不直接把 標(biāo)題和指示器 放到 Banner 的 Item 里面呢,這樣我們只要復(fù)寫 instantiateItem() 不就可以直接完成初始化了?嗯,關(guān)于這點(diǎn),只是為了切換效果好一點(diǎn),僅此而已,沒有什么額外的用意。

然后需要注意我們上面 小圓點(diǎn) 指示器使用了一個(gè) LinearLayout,這是因?yàn)槟承┣闆r下,我們預(yù)先可能不知道會(huì)有多少個(gè)頁面,所以我們干脆直接用一個(gè) LinearLayout,在代碼中動(dòng)態(tài)加載指示器的 view 添加進(jìn)來。

現(xiàn)在我們有了標(biāo)題和指示器,下面就要考慮如何讓這二者與頁面聯(lián)動(dòng)了。

這就用到了 addOnPageChangeListener()這個(gè)方法,該方法會(huì)設(shè)置一個(gè)OnPageChangeListener監(jiān)聽器,用來監(jiān)聽頁面的變化。其中有三個(gè)回調(diào)方法:

  1. onPageScrolled():當(dāng)前頁面發(fā)生滑動(dòng)時(shí)調(diào)用
  2. onPageSelected():頁面滑動(dòng)結(jié)束,選定頁面時(shí)調(diào)用。需要注意的是,該方法調(diào)用時(shí),動(dòng)畫未必完成
  3. onPageScrollStateChanged():當(dāng)滑動(dòng)狀態(tài)改變時(shí)調(diào)用,即處理何時(shí)開始滑動(dòng),或何時(shí)滑動(dòng)停止。

于是乎,我們只需要回調(diào)onPageSelected()方法即可,在此方法中設(shè)置標(biāo)題和指示器跟隨變化即可。

實(shí)例如下:

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

    }

    @Override
    public void onPageSelected(int position) {
        //處理指示器(小圓點(diǎn))的顯示邏輯
        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);
            }
        }
        //設(shè)置標(biāo)題
        bannerTitle.setText(titles[position]);
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }
});

關(guān)于頁面本身的加載,就只是用一個(gè) ArrayList<ImageView> 來存 Banner 的圖片資源,當(dāng)然為了順暢運(yùn)行,我是使用了 Glide 加載圖片(直接調(diào)用imageView.setImageResource(R.drawable.XXXX);時(shí)模擬器卡的動(dòng)不了,主要還是圖片資源太大了。= =),以下是實(shí)現(xiàn) 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));
    }
});

切換動(dòng)畫

切換動(dòng)畫,主要是用到setPageTransformer(boolean .. ,PageTransformer ...)方法來設(shè)置動(dòng)畫,該方法會(huì)接收一個(gè) PageTransfromer 參數(shù),這就是動(dòng)畫的核心關(guān)鍵所在了。

PageTransformer 實(shí)際上是一個(gè)接口,內(nèi)部只有一個(gè)方法 void transformPage(@NonNull View page, float position);,該方法接收兩個(gè)參數(shù),一個(gè) View 顯然就是我們的頁面了,當(dāng)然這個(gè) 頁面 涵蓋了當(dāng)前顯示的頁面、即將滑出的頁面、即將滑入的頁面以及隱藏的頁面,而這么多頁面,如何區(qū)分呢?這就第二個(gè)參數(shù) position 的作用了。首先,千萬不要和 ViewPager 下標(biāo)的 position 混淆了(float 類型你告訴我是下標(biāo)?),源碼中對(duì) position 的解釋如下:

View 的 position 和 ViewPager 當(dāng)前的中心位置有關(guān),當(dāng)前選中的頁面 position 是 0,前一個(gè)頁面是 -1,后一個(gè)頁面是 1。

但是有同學(xué)指出:

前后 item position 為 -1 和 1 的前提是你沒有給 ViewPager 設(shè)置 pageMargin。如果你設(shè)置了 pageMargin,前后 item 的 position 需要分別加上(或減去,前減后加)一個(gè)偏移量(偏移量的計(jì)算方式為 pageMargin / pageWidth)。

嗯,然后當(dāng)我們頁面滑動(dòng)的時(shí)候,position 是動(dòng)態(tài)變化的,transformPage()會(huì)根據(jù) position 的值來對(duì)頁面進(jìn)行屬性變換,position 的變化規(guī)律如下:(不考慮pageMargin,方便講解)

  1. position 分為三段:(-∞,-1)[-1,1](1,∞)
  2. 對(duì)于左右兩個(gè),多數(shù)時(shí)是不可見的,因此只需要分析以下[-1,1]區(qū)間
  3. 以第一頁->第二頁(左滑)為例:
    1. 頁1的position:0->-1
    2. 頁1的position:1->0
  4. 根據(jù)上述,我們就可以通過setAlpha()等方法設(shè)置屬性,以此達(dá)到自定義切換動(dòng)畫的效果。(實(shí)際和屬性動(dòng)畫有那么一點(diǎn)點(diǎn)類似)

實(shí)例嘛,見這節(jié)結(jié)束的實(shí)例就好了,此處不多搞了。

切換動(dòng)畫,可塑性實(shí)在是太高了,基本只有你想不到,沒有它做不到的,于是后面我們會(huì)再擴(kuò)充幾種切換動(dòng)畫來加深理解。

自動(dòng)輪播

自動(dòng)輪播,聽起來高大上,原理簡單的離譜:每隔一定時(shí)間給它一個(gè)事件,告訴它“嘿,你該切換頁面了”。嗯,說到這,不就是調(diào)用Handler.sendEmptyMessageDelayed(int what, long delayMillis)的小事了么~

Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        //mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//當(dāng)實(shí)現(xiàn)首尾循環(huán)無限輪播時(shí)的第一種方案時(shí)會(huì)這么設(shè)置,后面再說。
        mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mList.size());
        this.sendEmptyMessageDelayed(MSG_WHAT, 2000);
    }
};

有了上述代碼,我們只需要在初始化 ViewPager 之后調(diào)用依次Handler.sendEmptyMessageDelayed(int what, long delayMillis)就ok了。

當(dāng)然實(shí)踐中,我們可能需要對(duì)自動(dòng)輪播進(jìn)一步處理,譬如判斷滑動(dòng)手勢暫停輪播,我們總不會(huì)希望“我錯(cuò)過了一個(gè)感興趣的廣告,然后把頁面滑動(dòng)回去,結(jié)果很快頁面又!自動(dòng)滑動(dòng)回來了”,這種體驗(yàn)估計(jì)就很差。我在此處就不加以實(shí)現(xiàn)了,大家可以自行嘗試一下,畢竟我只是講解向~~(其實(shí)只是手勢判斷還沒接觸 ~~)。

首尾循環(huán)無限輪播

關(guān)于首尾無限輪播,指的是在第一個(gè)頁面時(shí)向左滑動(dòng)能夠連貫的滑動(dòng)到最后一頁,而在最后一頁向右滑動(dòng)時(shí),能順暢的滑動(dòng)到第一頁。

起初我是沒有注意到有什么坑的,但是當(dāng)我按照上面的代碼運(yùn)行之后,發(fā)現(xiàn)首尾十分的不連貫,會(huì)連續(xù)滑過中間的所有頁面,顯然并不能滿足我們的需求。

對(duì)于首尾循環(huán)的輪播,我也是參考網(wǎng)上的思路,就簡單介紹一下:

  1. 設(shè)置 ViewPager 展示的個(gè)數(shù)為Inreger.MAX_VALUE,初始化時(shí),將當(dāng)前頁面設(shè)置為n*mList.size(),除非閑得蛋疼,不然沒什么人有毅力滑個(gè)Integer.MAX_VALUE次吧,所以說通常是沒什么問題的。
  2. 在首尾分別加入最后一頁和當(dāng)前一頁,比如 原來是 a,b,c 現(xiàn)在變?yōu)?c,a,b,c,a,當(dāng)從末尾的c滑動(dòng)到a時(shí),將頁面切換為第一個(gè)a。同理在第一個(gè)a左滑動(dòng)到c時(shí),將頁面切換到第二個(gè)c。缺點(diǎn)可能就時(shí)可能會(huì)有短暫的延時(shí)?

貼出來參考的文章

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

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

實(shí)例

效果圖呈上:

這里寫圖片描述

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);//無限輪播時(shí)
            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);//設(shè)置緩存頁面數(shù)量

        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

見前幾節(jié)。

ViewPager 切換動(dòng)畫擴(kuò)充

ZoomOutPageTransformer

這里寫圖片描述

RotateDownPageTransformer

這里寫圖片描述

注意,為了再ViewPager中可以同時(shí)顯示多個(gè)頁面,我們需要再布局中 設(shè)置 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>

嗯,其他的好像沒什么可說得了(畢竟我的這兩個(gè)切換效果一個(gè)是摘自Google官方,一個(gè)摘自 鴻洋 大佬。。),就推薦一個(gè)兩個(gè)開源庫吧

  1. GitHub上比較火的廣告輪播控件,雖然是幾年前的東西,但還是很值得參考的:AndroidImageSlider
  2. 一個(gè)看起來還不錯(cuò)的切換效果合輯 PageTransformerHelp

另外,給出 鴻洋 大佬關(guān)于自定義切換效果的文章,大佬的文章還是很值得學(xué)習(xí)的。

巧用ViewPager 打造不一樣的廣告輪播切換效果

View Pager + Fragment + SharedPreferences 首次登錄引導(dǎo)界面

還是先將效果圖放出來吧(圖片和上面相同的資源,畢竟只是講解思路嘛~ 丑點(diǎn)就丑點(diǎn)吧~)
(為了圖省事,直接從CSDN把圖扒過來,然后又圖省事,在線壓縮gif,結(jié)果就來了兩重水印。。蛋碎了一地。)

實(shí)際上和上面也沒有什么本質(zhì)上的區(qū)別,所以在此就只介紹一下思路吧。

只是利用 SharedPreferences 來記錄當(dāng)前是否為第一次登錄,指示器和上述實(shí)現(xiàn)一致,同時(shí)加入兩個(gè)按鈕,右上角 skip(始終存在),指示器上方 got it(當(dāng)滑動(dòng)到最后一頁時(shí)出現(xiàn)),二者點(diǎn)擊時(shí)都會(huì)啟動(dòng)主頁面。

除此之外,該模式可以有很多變型:

  • 右上角 skip 倒計(jì)時(shí),倒計(jì)時(shí)完成后自動(dòng)啟動(dòng)主頁面,也可點(diǎn)擊進(jìn)入主頁面
  • 左右滑動(dòng)的頁面可以設(shè)置為 自動(dòng)輪播,播放到最后一頁時(shí) 自動(dòng)進(jìn)入主頁面
  • 不給 skip ,強(qiáng)制觀看完所有引導(dǎo)頁之后,才能通過彈出的got it 進(jìn)入主頁面
  • …………

代碼如下:(其實(shí)你會(huì)發(fā)現(xiàn),代碼和上面的代碼 差別很小~)

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>

總結(jié)

本文針對(duì) ViewPager 盡可能的介紹各種使用方法,涵蓋如下:

  • 基礎(chǔ)介紹
  • PagerAdapter + FragmentPagerAdapter&FragmentStatePagerAdapter
  • 與 Fragment + TabLayout 的聯(lián)動(dòng)使用
  • Banner 輪播圖
  • 自定義切換動(dòng)畫
  • 首次登錄引導(dǎo)界面

放上源碼地址,可以下載下來配合學(xué)習(xí)。

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

洋洋灑灑寫了這么多,最后愿本文對(duì)大家有所幫助。互勉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,932評(píng)論 25 709
  • ViewPager在開發(fā)中的使用頻率非常的高,所以在此做個(gè)總結(jié)。主要包括以下幾方面: ViewPager的簡介和作...
    西瓜太郎123閱讀 121,762評(píng)論 21 261
  • 2017年05月30 最后的懶加載寫的不好,推薦請(qǐng)叫我大蘇同學(xué)寫的Fragment懶加載博客,【Android】再...
    英勇青銅5閱讀 13,566評(píng)論 56 189
  • 昨天下午,下完班后我沒有直接回家,而是坐在辦公室里面看網(wǎng)頁,等到外面的太陽都被夜色吃掉了,我還是沒有起身。 這時(shí)候...
    生命溫度加1閱讀 695評(píng)論 0 1
  • 1.做好事會(huì)得到天助 今天幫姑姑拿藥,碰到了很多的好人,首先掛號(hào)掛方便門診的時(shí)候,那邊的志愿者對(duì)我很好,語氣溫柔,...
    真愛521閱讀 213評(píng)論 0 0

友情鏈接更多精彩內(nèi)容