學不動也要學!深刻了解ViewPager2

就在上個月20號(2019年11月20號),期待已久的ViewPager2 正式版終於發佈了!不知道你是否已經蠢蠢欲動着手用ViewPager2去改造你項目的ViewPager了?什麼?你還不知道ViewPager2?那麼請你立刻繫好安全帶,本篇文章將帶你一覽ViewPager2的風采。java

1、ViewPager2的新特性

ViewPager2從名字就能夠看出來它是ViewPager的升級版,既然是升級版那麼它相比ViewPager有哪些新功能和哪些API變化呢?咱們接着往下看。android

1.ViewPager2新特性

  • 基於RecyclerView實現。這意味着RecyclerView的優勢將會被ViewPager2所繼承。
  • 支持豎直滑動。只須要一個參數就能夠改變滑動方向。
  • 支持關閉用戶輸入。經過setUserInputEnabled來設置是否禁止用戶滑動頁面。
  • 支持經過編程方式滾動。經過fakeDragBy(offsetPx)代碼模擬用戶滑動頁面。
  • CompositePageTransformer 支持同時添加多個PageTransformer。
  • 支持DiffUtil ,能夠添加數據集合改變的item動畫。
  • 支持RTL (right-to-left)佈局。我以爲這個功能對國內開發者來講可能用處不大..

2.相比ViewPager變化的API

ViewPager2相比ViewPager作了哪些改變呢?研究了一番以後我大概列出如下幾點:git

  • ViewPager2與ViewPager同是繼承自ViewGrop,可是ViewPager2被聲明成了final。意味着咱們不可能再像ViewPager同樣經過繼承來修改ViewPager2的代碼。
  • FragmentStatePagerAdapter被FragmentStateAdapter 替代
  • PagerAdapter被RecyclerView.Adapter替代
  • addPageChangeListener被registerOnPageChangeCallback。咱們知道ViewPager的addPageChangeListener接收的是一個OnPageChangeListener的接口,而這個接口中有三個方法,當想要監聽頁面變化時須要重寫這三個方法。而ViewPager2的registerOnPageChangeCallback方法接收的是一個叫OnPageChangeCallback的抽象類,所以咱們能夠選擇性的重寫須要的方法便可。
  • 移除了setPargeMargin方法。
  • 關於offScreenPageLimit--離屏加載新特性

以上所羅列的新特性和API可能並不完整,若有疏漏能夠留言補充。github

2、開啓ViewPager2之旅

ViewPager2位於androidx包下,也就是它不像ViewPager同樣被內置在系統源碼中。所以,使用ViewPager2須要額外的添加依賴庫。另外,android support中不包含ViewPager,也就是要使用ViewPager2必須遷移到androidx才能夠。編程

1.添加依賴,目前ViewPager2的最新版本是1.0.0:

dependencies {
    implementation "androidx.viewpager2:viewpager2:1.0.0"
}
複製代碼

2.ViewPager2佈局文件:

<androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
複製代碼

3.ViewPager2的Adapter

由於ViewPager2內部封裝的是RecyclerView,所以它的Adapter也就是RecyclerView的Adapter。緩存

class MyAdapter : RecyclerView.Adapter<MyAdapter.PagerViewHolder>() {
    private var mList: List<Int> = ArrayList()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_page, parent, false)
        return PagerViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: PagerViewHolder, position: Int) {
        holder.bindData(mList[position])
    }

    fun setList(list: List<Int>) {
        mList = list
    }

    override fun getItemCount(): Int {
        return mList.size
    }
	// ViewHolder須要繼承RecycleView.ViewHolder
    class PagerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val mTextView: TextView = itemView.findViewById(R.id.tv_text)
        private var colors = arrayOf("#CCFF99","#41F1E5","#8D41F1","#FF99CC")

        fun bindData(i: Int) {
            mTextView.text = i.toString()
            mTextView.setBackgroundColor(Color.parseColor(colors[i]))
        }
    }
}
複製代碼

item_page中代碼以下:安全

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <TextView
        android:id="@+id/tv_text"
        android:background="@color/colorPrimaryDark"
        android:layout_width="match_parent"
        android:layout_height="280dp"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="22sp" />
</LinearLayout>
複製代碼

4.在Activity中爲ViewPager設置Adapter:

val viewPager2 = findViewById<ViewPager2>(R.id.view_pager)
        val myAdapter = MyAdapter()
        myAdapter.setList(data)
        viewPager2.adapter = myAdapter
複製代碼

很簡單就完成了一個ViewPager的功能,來看下效果怎麼樣: app

在這裏插入圖片描述

5.ViewPager2豎直滑動

接下來咱們經過一行代碼爲其設置豎直滑動less

viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
複製代碼

豎直滑動用ViewPager是很難實現的,而經過ViewPager2只須要設置一個參數便可。來看下效果: ide

在這裏插入圖片描述

6.頁面滑動事件監聽

上文已經提到過了,咱們爲ViewPager設置頁面滑動的監聽事件須要重寫三個方法,而爲ViewPager2設置監聽事件只須要重寫須要的方法便可,由於ViewPager2中OnPageChangeCallback是一個抽象類。

viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                Toast.makeText(this@MainActivity, "page selected $position", Toast.LENGTH_SHORT).show()
            }
        })
複製代碼

7.setUserInputEnabled與fakeDragBy

咱們知道,在使用ViewPager的時候想要禁止用戶滑動須要重寫ViewPager的onInterceptTouchEvent。而ViewPager2被聲明爲了final,咱們沒法再去繼承ViewPager2。那麼咱們應該怎麼禁止ViewPager2的滑動呢?其實在ViewPager2中已經爲咱們提供了這個功能,只須要經過setUserInputEnabled便可實現。

viewPager2.isUserInputEnabled = false
複製代碼

同時ViewPager2新增了一個fakeDragBy的方法。經過這個方法能夠來模擬拖拽。在使用fakeDragBy前須要先beginFakeDrag方法來開啓模擬拖拽。fakeDragBy會返回一個boolean值,true表示有fake drag正在執行,而返回false表示當前沒有fake drag在執行。咱們經過代碼來嘗試下:

fun fakeDragBy(view: View) {
        viewPager2.beginFakeDrag()
        if (viewPager2.fakeDragBy(-310f))
            viewPager2.endFakeDrag()
    }
複製代碼

須要注意到是fakeDragBy接受一個float的參數,當參數值爲正數時表示向前一個頁面滑動,當值爲負數時表示向下一個頁面滑動。 下面來看下效果圖:

在這裏插入圖片描述
演示圖中禁止了用戶輸入,經過按鈕點擊能夠模擬用戶滑動。

8.ViewPager2的offScreenPageLimit

offScreenPageLimit在ViewPager中就已經存在,這個參數用來控制ViewPager左右兩端預加載頁面的個數。爲了保證ViewPager的流暢性,offScreenPageLimit被強制規定爲大於0的數,即便咱們將其設置爲0,ViewPager內部也會將其改成1。所以ViewPager就被強制左右兩邊至少加載一個頁面。這也是一直被廣大開發者所詬病的一個問題。而在ViewPager2中針對這一問題作了優化。咱們點開ViewPager2的源碼來看下:

# VewPager2
    
    private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
    
    /**
     * Value to indicate that the default caching mechanism of RecyclerView should be used instead
     * of explicitly prefetch and retain pages to either side of the current page.
     * @see #setOffscreenPageLimit(int)
     */
    public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
    
     /** @hide */
    @SuppressWarnings("WeakerAccess")
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Retention(SOURCE)
    @IntDef({OFFSCREEN_PAGE_LIMIT_DEFAULT})
    @IntRange(from = 1)
    public @interface OffscreenPageLimit {
    }
    
複製代碼

能夠看到在ViewPager2中offScreenPageLimit的默認值被設置爲了-1,並且offScreenPageLimit這個成員變量被一個名爲@OffscreenPageLimit的註解所修飾,而在這個註解強制要求int的範圍是大於等於1的。什麼?ViewPager2的預加載頁面難道也必須大於等於1?那這相比ViewPager有什麼區別呢?先彆着急,其實最大的區別就在這個OFFSCREEN_PAGE_LIMIT_DEFAULT上,這個值被設置爲-1,那麼它表明什麼意思呢?咱們能夠從ViewPager2源碼的註釋中找出一些端倪

/** * <p>Set the number of pages that should be retained to either side of the currently visible * page(s). Pages beyond this limit will be recreated from the adapter when needed. Set this to * {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} to use RecyclerView's caching strategy. The given value * must either be larger than 0, or {@code #OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p> * * <p>Pages within {@code limit} pages away from the current page are created and added to the * view hierarchy, even though they are not visible on the screen. Pages outside this limit will * be removed from the view hierarchy, but the {@code ViewHolder}s will be recycled as usual by * {@link RecyclerView}.</p> * * <p>This is offered as an optimization. If you know in advance the number of pages you will * need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting * can have benefits in perceived smoothness of paging animations and interaction. If you have a * small number of pages (3-4) that you can keep active all at once, less time will be spent in * layout for newly created view subtrees as the user pages back and forth.</p> * * <p>You should keep this limit low, especially if your pages have complex layouts. By default * it is set to {@code OFFSCREEN_PAGE_LIMIT_DEFAULT}.</p> * * @param limit How many pages will be kept offscreen on either side. Valid values are all * values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} * @throws IllegalArgumentException If the given limit is invalid * @see #getOffscreenPageLimit() */
    public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
        if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            throw new IllegalArgumentException(
                    "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
        }
        mOffscreenPageLimit = limit;
        // Trigger layout so prefetch happens through getExtraLayoutSize()
        mRecyclerView.requestLayout();
    }
複製代碼

從這段對setOffscreenPageLimit(int)方法的註釋中咱們能夠看到,當setOffscreenPageLimit被設置爲OFFSCREEN_PAGE_LIMIT_DEFAULT時候會使用RecyclerView的緩存機制。那麼咱們就來在ViewPager2中嘗試下加載Fragment是一種怎樣的效果吧。 首先咱們在ViewPager中添加多個Fragment,而且setOffscreenPageLimit使用默認值,而後再Fragment聲明週期中打印出日誌,代碼再也不貼出,直接看日誌打印的內容:

從日誌中能夠看出來初始化的只有第一個Fragment。當咱們滑動到第二個頁面的時候,打印日誌以下:

能夠看到第二個頁面在滑動的時候才被初始化,由此咱們能夠看出在ViewPager2中默認的OffscreenPageLimit是不會進行頁面預加載的。 接下來咱們將offscreenPageLimit值改成1,再來看下輸出日誌:

此時能夠看到offscreenPageLimit設置爲1後會預加載進來一個頁面,和ViewPager幾乎是同樣的效果。總之,ViewPager2對於ViewPager的預加載機制作了優化,使得體驗上變得更好。關於ViewPager2的offScreenLimit在本篇文章中再也不深究,我會在下篇文章中深刻探討。

3、ViewPager2的PageTransformer

相比ViewPager,ViewPager2的Transformer功能有了很大的擴展。ViewPager2不只能夠經過PageTransformer用來設置頁面動畫,還能夠用PageTransformer設置頁面間距以及同時添加多個PageTransformer。接下來咱們就來認識下ViewPager2的PageTransformer吧!

1.setPageMargin

在第一章中咱們提到了ViewPager2移除了setPageMargin方法,那麼怎麼爲ViewPager2設置頁面間距呢?其實在ViewPager2中爲咱們提供了MarginPageTransformer,咱們能夠經過ViewPager2的setPageTransformer方法來設置頁面間距。代碼以下:

viewPager2.setPageTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
複製代碼

上述代碼咱們爲ViewPager2設置了10dp的頁面間距。效果以下:

在這裏插入圖片描述

2.認識CompositePageTransformer

這個時候咱們應該有個疑問,爲ViewPager2設置了頁面間距後若是還想設置頁面動畫的Transformer怎麼辦呢?這時候就該CompositePageTransformer出場了。從名字上也能夠看出來它是一個組合的PageTransformer。沒錯,CompositePageTransformer實現了PageTransformer接口,同時在其內部維護了一個List集合,咱們能夠將多個PageTransformer添加到CompositePageTransformer中。

val compositePageTransformer = CompositePageTransformer()
        compositePageTransformer.addTransformer(ScaleInTransformer())
        compositePageTransformer.addTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
        viewPager2.setPageTransformer(compositePageTransformer)
複製代碼

上述代碼中咱們經過CompositePageTransformer爲ViewPager設置了MarginPageTransformer和一個頁面縮放的ScaleInTransformer。來看下效果:

在這裏插入圖片描述

3.ViewPager2中的PageTransformer

PageTransformer是一個位於ViewPager2中的接口,所以ViewPager2的PageTransformer是獨立於ViewPager的,它與ViewPager的PageTransformer沒有任何關係。雖然如此,卻沒必要擔憂。由於ViewPager2的PageTransformer和ViewPager的PageTransformer實現方式如出一轍。咱們看下上一小節中用到的ScaleInTransformer:

class ScaleInTransformer : ViewPager2.PageTransformer {
    private val mMinScale = DEFAULT_MIN_SCALE
    override fun transformPage(view: View, position: Float) {
        view.elevation = -abs(position)
        val pageWidth = view.width
        val pageHeight = view.height

        view.pivotY = (pageHeight / 2).toFloat()
        view.pivotX = (pageWidth / 2).toFloat()
        if (position < -1) {
            view.scaleX = mMinScale
            view.scaleY = mMinScale
            view.pivotX = pageWidth.toFloat()
        } else if (position <= 1) {
            if (position < 0) {
                val scaleFactor = (1 + position) * (1 - mMinScale) + mMinScale
                view.scaleX = scaleFactor
                view.scaleY = scaleFactor
                view.pivotX = pageWidth * (DEFAULT_CENTER + DEFAULT_CENTER * -position)
            } else {
                val scaleFactor = (1 - position) * (1 - mMinScale) + mMinScale
                view.scaleX = scaleFactor
                view.scaleY = scaleFactor
                view.pivotX = pageWidth * ((1 - position) * DEFAULT_CENTER)
            }
        } else {
            view.pivotX = 0f
            view.scaleX = mMinScale
            view.scaleY = mMinScale
        }
    }

    companion object {

        const val DEFAULT_MIN_SCALE = 0.85f
        const val DEFAULT_CENTER = 0.5f
    }
}
複製代碼

4.ViewPager2的一屏多頁效果

在ViewPager2的官方Sample上看到了ViewPager2的一屏多頁能夠經過爲RecyclerView設置Padding來實現。代碼以下:

viewPager2.apply { 
                    offscreenPageLimit=1
                   val recyclerView= getChildAt(0) as RecyclerView
                    recyclerView.apply {
                        val padding = resources.getDimensionPixelOffset(R.dimen.dp_10) +
                                resources.getDimensionPixelOffset(R.dimen.dp_10)
                        // setting padding on inner RecyclerView puts overscroll effect in the right place
                        setPadding(padding, 0, padding, 0)
                        clipToPadding = false
                    }
                }
        val compositePageTransformer = CompositePageTransformer()
        compositePageTransformer.addTransformer(ScaleInTransformer())
        compositePageTransformer.addTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
        viewPager2.setPageTransformer(compositePageTransformer)

複製代碼

最後,咱們來看下效果

在這裏插入圖片描述

4、ViewPager2與Fragment

咱們前面也已經提到了ViewPager2中新增的FragmentStateAdapter 替代了ViewPager的FragmentStatePagerAdapter。那麼來咱們就用ViewPager2來實現一個Activity中嵌套Fragment的實例。

1.Activity的layout中添加ViewPager2

<androidx.viewpager2.widget.ViewPager2
            android:id="@+id/vp_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@id/rg_tab" />
複製代碼

2.實現FragmentStateAdapter

class AdapterFragmentPager(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {

    private val fragments: SparseArray<BaseFragment> = SparseArray()

    init {
        fragments.put(PAGE_HOME, HomeFragment.getInstance())
        fragments.put(PAGE_FIND, PageFragment.getInstance())
        fragments.put(PAGE_INDICATOR, IndicatorFragment.getInstance())
        fragments.put(PAGE_OTHERS, OthersFragment.getInstance())
    }

    override fun createFragment(position: Int): Fragment {
        return fragments[position]
    }

    override fun getItemCount(): Int {
        return fragments.size()
    }

    companion object {

        const val PAGE_HOME = 0

        const val PAGE_FIND = 1

        const val PAGE_INDICATOR = 2

        const val PAGE_OTHERS = 3

    }
}
複製代碼

3.在Activity中爲ViewPager2設置FragmentStateAdapter

vp_fragment.adapter = AdapterFragmentPager(this)
        vp_fragment.offscreenPageLimit = 3
        vp_fragment.isUserInputEnabled=false
複製代碼

5、小結

本篇文章咱們認識了ViewPager2的新特性以及其用法。總得來講ViewPager2相比ViewPager無論在性能上仍是在功能上都有了很大的提高。所以,我相信在不久的將來ViewPager2一定會取代ViewPager。那麼,你是否已經考慮將ViewPager2用到你的項目中了呢?

最後再來給你們推薦一下BannerViewPager。這是一個基於ViewPager實現的具備強大功能的無限輪播庫。在將來,我會在BannerViewPager 3.0版本中用ViewPager2來重構代碼。歡迎你們到GitHub關注BannerViewPager

本文涉及源碼下載

第四節中ViewPager2與Fragment的代碼見:

BannerViewPager

相關文章
相關標籤/搜索