ViewPager 超詳解:玩出十八般花樣

OCN_Yang發表於2018-01-26

授權宣告:本文已授權微信公眾號:鴻洋(hongyangAndroid)原創首發。其他轉載請再諮詢作者許可!

ViewPager 超詳解:玩出十八般花樣

雖然沒有 RecyclerView 這種列表控制元件常用些,但是在開發中你ViewPager 肯定也是不可或缺的控制元件,引導頁、輪播圖、卡片畫廊等效果總是缺少不了 ViewPager 的身影。
相信每一位朋友對 ViewPager 的基礎使用都已經很熟練了,今天在這裡就從簡至繁將 ViewPager 的每個用法都梳理一邊。

主要包括以下內容:

  • ViewPager 基本使用(簡介、介面卡)
  • ViewPager + TabLayout + Fragment 的使用
  • ViewPager 輪播圖的使用(指示器、標題、自動輪播、首尾迴圈)
  • ViewPager 的切換效果(PageTransformer)
  • ViewPager 切換效果進階

ViewPager 的基礎使用

對於 ViewPager ,官方的描述大概是這樣的:頁面允許左右滑動的佈局管理器,而不同頁面帶有不同的資料。

這裡簡單歸結如下:

  • ViewPager 是 v4 包中的一個類。
  • ViewPager 類直接繼承了 ViewGroup 類,它是一個容器類,可以在其中新增其他的 view 。
  • 類似於 ListView,也有自己的介面卡,用來填充資料頁面。

關於 ViewPager 在佈局檔案中的宣告,這裡就不再說了。其實是沒什麼好說的,並沒有什麼可以直接宣告的特殊屬性,由於繼承於 ViewGroup 有的也都是些 ViewGroup 的屬性。

這裡值得介紹的也就是幾個可以動態設定方法了,常用的有以下幾個:

  • setAdapter(PagerAdapter adapter) 設定介面卡
  • setOffscreenPageLimit(int limit) 設定快取的頁面個數,預設是 1
  • setCurrentItem(int item) 跳轉到特定的頁面
  • setOnPageChangeListener(..) 設定頁面滑動時的監聽器(現在API中建議使用 addOnPageChangeListener(..)
  • setPageTransformer(..PageTransformer) 設定頁面切換時的動畫效果
  • setPageMargin(int marginPixels) 設定不同頁面之間的間隔
  • setPageMarginDrawable(..) 設定不同頁面間隔之間的裝飾圖也就是 divide ,要想顯示設定的圖片,需要同時設定 setPageMargin()

謹記上面這幾個方法,玩轉 ViewPager 其實都是圍繞它們進行的,能不能玩出花樣,就看你把它們運用的怎麼樣了。

上面的方面大多一看說明就明白了,這裡值得一提的就是 ViewPager 的介面卡了。

PagerAdapter

PagerAdapter 是抽象的類,所以使用時只能使用它的子類,實現子類必須要實現以下四個方法:

  • getCount(); 是獲取當前窗體介面數,也就是資料的個數。
  • isViewFromObject(View view, Object object); 這個方法用於判斷是否由物件生成介面,官方建議直接返回 return view == object;
  • instantiateItem(View container, int position); 要顯示的頁面或需要快取的頁面,會呼叫這個方法進行佈局的初始化。
  • destroyItem(ViewGroup container, int position, Object object); 如果頁面不是當前顯示的頁面也不是要快取的頁面,會呼叫這個方法,將頁面銷燬。

相信大家對上面這些方法的實現並不陌生,這裡就不詳細介紹了。另外我們知道官方給我們提供的還有 PagerAdapter 的兩個直接子類 FragmentPagerAdapter 和 FragmentStatePagerAdapter 。而我們常常會在 ViewPager 和 Fragment 結合使用的時候來使用這兩個介面卡。具體的用法和它們之間的區別,我們在下個章節講。

ViewPager + TabLayout + Fragment 的結合使用

在引導頁中我們常常用 ViewPager 和 Fragment 結合使用,而像新聞分類的頁面我們會再加上一個 TabLayout 三者聯動使用。而此時,我們不會再使用 PagerAdapter 了,而是直接使用官方提供的專門用於與 Fragment 結合使用的 FragmentPagerAdapter。

FragmentPagerAdapter 它將每一個頁面表示為一個 Fragment,並且每一個 Fragment 都將會儲存到 FragmentManager 當中。而且,當使用者沒可能再次回到頁面的時候,FragmentManager 才會將這個 Fragment 銷燬。

使用 FragmentPagerAdapter 需要實現兩個方法:

  • public Fragment getItem(int position) 返回的是對應的 Fragment 例項,一般我們在使用時,會通過構造傳入一個要顯示的 Fragment 的集合,我們只要在這裡把對應的 Fragment 返回就行了。
  • public int getCount() 這個上面介紹過了返回的是頁面的個數,我們只要返回傳入集合的長度就行了。

使用起來是非常簡單的,FragmentStatePagerAdapter 的使用也和上面一樣,那兩者到底有什麼區別呢?

區別如下:

  • FragmentPagerAdapter:對於不再需要的 fragment,選擇呼叫 onDetach() 方法,僅銷燬檢視,並不會銷燬 fragment 例項。
  • FragmentStatePagerAdapter:會銷燬不再需要的 fragment,噹噹前事務提交以後,會徹底的將 fragmeng 從當前 Activity 的FragmentManager 中移除,state 標明,銷燬時,會將其 onSaveInstanceState(Bundle outState) 中的 bundle 資訊儲存下來,當使用者切換回來,可以通過該 bundle 恢復生成新的 fragment,也就是說,你可以在 onSaveInstanceState(Bundle outState) 方法中儲存一些資料,在 onCreate 中進行恢復建立。

由上總結:
使用 FragmentStatePagerAdapter 更省記憶體,但是銷燬後新建也是需要時間的。一般情況下,如果你是製作主頁面,就 3、4 個 Tab,那麼可以選擇使用 FragmentPagerAdapter,如果你是用於 ViewPager 展示數量特別多的條目時,那麼建議使用 FragmentStatePagerAdapter。

那 Tablayout 如何和 Viewpager 聯動呢?由於我們這裡主要是講解 ViewPager 的,所謂 “術業有專攻” 所以關於 TabLayout 的使用我們就不再摻和了。
第一步,初始化 TabLayout 和 ViewPager 後只要通過呼叫 TabLayout 的 tabLayout.setupWithViewPager(viewPager) 方法就將兩者繫結在一起了。
第二步,重寫 PagerAdapter 的 public CharSequence getPageTitle(int position) 方法,而 TabLayout 也正是通過 setupWithViewPager() 方法底部會呼叫 PagerAdapter 中的getPageTitle() 方法來實現聯動的。

ViewPager 輪播圖的使用

關於此章本想給大家細細到來,才寫上面兩章都這麼多篇幅了,我們還有給下面兩章重點講的部分留點空間呢。如果非得想看還不嫌我囉嗦,那我有時間再把這段給不出來。

這章就這樣結束,當然沒有,雖然不負責任,但是也不能撩完妹子就閃人啊!這裡還是要基本原理給大家論道論道的。

Banner 元素組成圖

從上圖我們可以知道,一般我們使用 ViewPager 做 Banner 時主要有以上幾個元素:

標題 & 指示器
我們可以把標題和指示器直接寫在我們 Banner 的 item 的佈局中,這樣通過在 PageAdapter 的 instantiateItem() 方法初始化頁面時,直接設定。但是一般我們不會這樣做(如果標題沒有陰影的話,可以如上面說的那樣),因為這樣在頁面滑動的時候,會顯得特別生硬,尤其是指示器。
那該如何呢?一般我們會在 ViewPager 所在的佈局檔案中,宣告指示器和標題佈局,如下:

<FrameLayout
     ...>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        ..."/>

    <LinearLayout
        android:layout_gravity="bottom"
        ...>
		<!--指示器佈局,因為不知道 item 的個數,所以會動態的把指示器的View新增到這裡-->
        <LinearLayout
            android:id="@+id/bannerIndicators"
            .../>
		<!--標題-->
        <TextView
            android:id="@+id/bannerTitle"
            .../>
    </LinearLayout>

</FrameLayout>
複製程式碼

那如何才能實現當頁面滑動時,標題和指示器伴隨改變呢?還記不記得,一開始介紹 ViewPager 時,它有一個可以設定監聽頁面改變的方法 addOnPageChangeListener(),在 OnPageChangeListener 監聽器中有一個頁面滑動結束時的回撥方法 onPageSelected(int position) ,我們只需要在這個方法中,來設定標題和指示器跟隨變化就行了。

自動輪播
實現自動輪播的原理其實更簡單,只要我們每隔一定時間傳送一個切換頁面的事件就行了。實現這個功能有很多種方法,相信作為 Android 開發者,你最快想到的就是 Handler.sendEmptyMessageDelayed(int what, long delayMillis) 了吧。

Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (mAutoPlay) {
                //mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);//無限輪播時
				mViewPager.setCurrentItem((mViewPager.getCurrentItem()+1) % mViewPagerItemCount)
                this.sendEmptyMessageDelayed(MSG_WHAT, delayMillis);
            }
        }
    };
複製程式碼

當然不要忘了在外部,初始化完成後呼叫一次mHandler.sendEmptyMessageDelayed(MSG_WHAT, delayMillis);

首尾迴圈無限輪播
當然,我們在設定自動輪播時,已經做到了首尾迴圈無限輪播了呀。

其實這裡所說的無限輪播是指:當我們手動滑到最後一個頁面時,依然可以向後伴隨手指滑動,並跳轉顯示的是第一個頁面;反之滑到首個頁面也是一樣。
旁邊那位腦子靈光的大兄弟又說了,那還不簡單,在頁面監聽器 OnPageChangeListener 裡,通過 position 分辨滑動的是不是首頁或者最後一頁,然後通過 setCurrentItem() 設定一下不就行了,還那麼麻煩。
大胸弟,你且消消氣!這裡並不使用這種方式是因為,這種方式的跳轉是十分生硬的,同時是不能實現“伴隨手指滑動”這個條件的。

那究竟如何如何才能實現呢?目前江湖流傳的有一下兩種方法:

  1. 使 adapter 的 getCount() 返回 Integer.MAX_VALUE,再在初始化時設定當前頁面為幾千頁(如:ViewPager.setCurrentItem(1000*data.size)),其實就是障眼法,大爺心情好的話,向前滑動幾千頁也不是不可能的;
  2. 通過監聽 viewpager 的滑動來設定頁面。如當前有資料 123,則設定頁面為 31231,當頁面滑動到第一個 3 時,設定當前頁面為第二個 3,那麼左右都可以滑動,當其滑動到第二個 1 時同理。

關於第一種是目前流行最廣的方法,如果大家想檢視詳細的說明可以參考下面這篇文章(網上隨便找的,對可靠性不作擔保啊):
ViewPager真正的無限輪播

關於第二種方式,嚴格意義上分析是會在滑動過程中產生生硬的跳動的。不過有位江湖義士聲稱已經解決了這種不和諧情況的發生,附上文章地址(可靠性更不作擔保啊):
打造真正的無限迴圈viewpager (不負責的我真的沒有測試這個可靠性,大家閒的測試下,如果效果不好的話,告訴我,我趕快把這個連結刪除~~)

自定義 ViewPager 的切換效果

本來最近封裝一個了 ViewPager 十八般花樣、樣樣都有的 PageTransformer 動效庫,想著前面少囉嗦點,然後把這章作為重點來講的。沒想到前面還是囉裡囉嗦這麼多(恍然間,我好像找到自己一直撩妹不成功單身的原因了~),好了,步入正題。

關於 ViewPager 的切換動畫,官方提供了一個內部介面 ViewPager.PageTransformer 來供我們實現自定義切換動效。這個介面裡只提供了一個方法 public void transformPage(View view, float position),但是千萬不要小看了這兩個方法,這裡面的道道有很多呢。

transformPage 方法兩個引數,一個是 View ,這個好理解就是當前要設定動效的頁面。這個頁面並不單單是指當前顯示的頁面,即將滑出的頁面、即將滑入的頁面、已經隱藏的頁面,也就是說這個 View 是指所有的頁面。那如何分辨 View 到底是指哪個頁面呢,這個需要根據第二個引數 position 來辨別。

你千萬不要把 position 理解成了 ViewPager 頁面的下標,一定要看仔細,這個 position 可是 float 型別,下標怎麼可能是浮點型呢!

從 doc 註釋來看,當前選中的 item 的 position 永遠是 0 ,被選中 item 的前一個為 -1,被選中 item 的後一個為 1。

其實這裡文件的描述並不是完全正確的,前後 item position 為 -1 和 1 的前提是你沒有給 ViewPager 設定 pageMargin。
如果你設定了 pageMargin,前後 item 的 position 需要分別加上(或減去,前減後加)一個偏移量(偏移量的計算方式為 pageMargin / pageWidth)。

在使用者滑動介面的時候,position 是動態變化的,下面以左滑為例(以向左為正方向):

  • 選中 item 的 position:從 0 漸至 -1 - offset (pageMargin / pageWidth)
  • 前一個 item 的 position:從 -1 漸至 -1 - offset (pageMargin / pageWidth)
  • 前兩個 item 的 position:從 -2 漸至 -2 - offset (pageMargin / pageWidth),再往前就以此類推
  • 後一個 item 的 position:從 1 + offset (pageMargin / pageWidth) 漸至 0,再往後就以此類推

每一次滑動,每個 View 對應的 position 是一個在一個區間範圍內動態漸變的過程,所以我們可以將 position 的值應用於 setAlpha(), setTranslationX(), 或者 setScaleY() 等等方法,從而實現自定義的切換動畫有一個漸變的效果。

這裡給大家舉一個視差切換動效的實現方式,我們先來看一下效果:

ViewPager 超詳解:玩出十八般花樣

其實實現起來很簡單,就是滑動時給頁面再設定一個頁面橫向滑動的動畫,讓頁面實現滑動的速度慢於手指滑動的速度,這樣就會有種視差的效果:

@Override
public void transformPage(View page, float position) {
    int width = page.getWidth();
	//我們給不同狀態的頁面設定不同的效果
	//通過position的值來分辨頁面所處於的狀態
    if (position < -1) {//滑出的頁面
        page.setScrollX((int) (width * 0.75 * -1));
    } else if (position <= 1) {//[-1,1]
        if (position < 0) {//[-1,0]
            page.setScrollX((int) (width * 0.75 * position));
        } else {//[0,1]
            page.setScrollX((int) (width * 0.75 * position));
        }
    } else {//即將滑入的頁面
        page.setScrollX((int) (width * 0.75));
    }
}
複製程式碼

其實這裡處於大於1或者小於-1的狀態的頁面都好理解。需要詳細講解的就是 [-1,1] 這個區間狀態的頁面。大家可以想象一下:

像左滑動

由上圖可以看出,當滑動時,(如果沒有偏移量)介面上最多出現兩個 item,一個即將滑出即將隱藏的頁面(postion變化為:從0漸到-1),一個滑入即將完全顯示的頁面(postion變化為:從1漸到0)。

這樣給每個不同狀態的頁面設定不同的動效就達到我們想要的目的了。回到上面例子中的程式碼,剛才那位大兄弟又說話了,你的程式碼明明 [0 -> -1] 和 [1 -> 0] 兩個狀態的 item 設定的是一樣的動效啊。這裡只是程式碼一樣,動效實際上是不一樣的,因為一個position 是大於 0 的,一個 position 是小於 0 的,滑動的方向自然是相反的。難道你非得讓我寫成 page.setScrollX((int) (width * -0.75 * -position)) 這樣嗎?

想實現更多炫酷的動效,可以檢視為大家封裝好的 PageTransformerHelp 庫,GitHub地址:github.com/OCNYang/Pag…

ViewPager 切換效果進階

其實上面已經把該講的自定義的方面都講的很清楚了;整個梳理下來,上面一開始給大家列舉的著重強調的幾個 ViewPager 的動態設定的方法就剩 setPageMargin(int marginPixels) 沒有說了,那麼這個方法又會給我們帶來什麼樣的神奇效果呢?

介面能夠同時看到多個 item

那是如何實現上面這種效果呢,可以肯定的是:兩邊兩個 item 被縮小且透明度變低,是通過設定上面所說的自定義動效完成的。那如何在一個介面能夠同時看到 3 個 item 呢?那麼就通過下圖的分析和解釋告訴實現的原理:

卡片式輪播效果實現原理分析

看了上面的解釋有沒有一種恍然大悟,迫不及待想試試的衝動?如果通過上圖仍然略有疑惑可以看看鴻洋大神的這篇文章,可以說是很全面了:
巧用ViewPager 打造不一樣的廣告輪播切換效果

結尾

到此,ViewPager 的基本使用方式已經講的差不多了。想檢視更多 切換動畫 的效果,可以到本文的原始碼地址進行檢視。

原始碼地址github.com/OCNYang/Pag…

參考文章:
blog.csdn.net/lmj62356579…
blog.csdn.net/qq_30716173…

相關文章