RecyclerView擴(kuò)展(五) - ViewPager2的源碼分析

??ViewPager2是Google爸爸在幾個(gè)月前推出來的新控件,此控件的目的就是為了替代傳統(tǒng)的ViewPager控件杰标。至于為什么要淘汰ViewPager,我想就不用解釋這其中的原因吧彩匕,ViewPager歷來最大的詬病就是不會(huì)復(fù)用View(其實(shí)我對ViewPager的原理了解的不多腔剂,各位大佬就當(dāng)我信口雌黃吧????。)驼仪。而ViewPager2內(nèi)部是通過RecyclerView來實(shí)現(xiàn)的掸犬,性能當(dāng)然不容置疑袜漩。還有最重要的一點(diǎn),ViewPager2幾乎復(fù)制了ViewPager所有的API湾碎,所以宙攻,ViewPager2在使用上幾乎跟ViewPage完全一樣。
??本文打算從源碼角度入手介褥,詳細(xì)的分析ViewPager2的實(shí)現(xiàn)原理座掘。其實(shí)早在RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關(guān)組件的源碼分析文章中,我在分析SnapHelper源碼時(shí)柔滔,在文章里面簡單的說了一句溢陪。而此文算是兌現(xiàn)當(dāng)初的一個(gè)承諾,看看怎么通過RecyclerView + SnapHelper的方式來實(shí)現(xiàn)一個(gè)ViewPager睛廊。
??需要注意的是:目前ViewPager2還不太穩(wěn)定形真,所以請謹(jǐn)慎使用到生產(chǎn)環(huán)境中。
??在閱讀本文之前喉前,建議大家先了解SnapHelper的原理没酣,本文參考文章:

  1. RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關(guān)組件的源碼分析

??注意王财,本文ViewPager2版本均為1.0.0-alpha04

1. 概述

??我在閱讀ViewPager2的源碼之前卵迂,思考過一個(gè)問題,到底應(yīng)不應(yīng)該看看ViewPager2的源碼嗎绒净?其實(shí)從簡單的方面來說见咒,真的沒必要去閱讀它的源碼,熟悉RecyclerView的同學(xué)挂疆,ViewPager2內(nèi)部肯定是使用SnapHelper實(shí)現(xiàn)改览。所以,我們閱讀ViewPager2的源碼到底是為了什么缤言?就是因?yàn)殚e的蛋疼宝当,然后寫出來裝逼嗎?我想肯定不是胆萧,我總結(jié)如下幾點(diǎn):

  1. 了解ViewPager2是怎么將RecyclerView的滑動(dòng)事件轉(zhuǎn)變?yōu)?code>ViewPager的頁面滑動(dòng)事件庆揩。
  2. 了解怎么使用RecyclerView來加載Fragment。

??這其中跌穗,我覺得第2點(diǎn)非常的重要订晌,為什么重要呢?RecyclerView加載Fragment這里涉及到細(xì)節(jié)非常的多,因?yàn)镕ragment本身有生命周期蚌吸,所以我們?nèi)绾瓮ㄟ^Adapter來有效維護(hù)Fragment的生命周期锈拨,這本身就是一種挑戰(zhàn)。
??本文打算從如下幾個(gè)方面來介紹:

  1. PagerSnapHelper的源碼分析羹唠,主要是了解它內(nèi)部的原理奕枢,是如何實(shí)現(xiàn)ViewPager的效果娄昆。
  2. 各種組件的分析,包括ScrollEventAdapter缝彬、PageTransformerAdapter稿黄。
  3. FragmentStateAdapter的源碼分析,主要是了解Adapter是怎么加載Fragment的跌造。

??接下來杆怕,我們正式來分析ViewPager2的源碼分析。

2. ViewPager2的基本結(jié)構(gòu)

??在分析ViewPager2源碼之前壳贪,我們先來看看ViewPager的內(nèi)部結(jié)構(gòu)陵珍,了解一下ViewPager2是怎么實(shí)現(xiàn)的。
??從ViewPager2的源碼中我們知道违施,ViewPager2繼承于ViewGroup互纯,其內(nèi)部包含有一個(gè)RecyclerView控件,其他部分都是圍繞著這個(gè)RecyclerView來實(shí)現(xiàn)的磕蒲×袅剩總之,ViewPager2是以一個(gè)組合的方式來實(shí)現(xiàn)的辣往。
??這其中兔院,ScrollEventAdapter的作用是將RecyclerView.OnScrollListener事件轉(zhuǎn)變?yōu)?code>ViewPager2.OnPageChangeCallback事件;FakeDrag的作用是用來實(shí)現(xiàn)模擬拖動(dòng)的效果站削;PageTransformerAdapter的作用是將頁面的滑動(dòng)事件轉(zhuǎn)變?yōu)楸嚷首兓宦埽热缯f,一個(gè)頁面從左到右滑動(dòng)许起,變化規(guī)則是從0~1十偶,關(guān)于這個(gè)組件,我相信熟悉ViewPager2的同學(xué)都應(yīng)該都知道园细。
??最后就是最重要的東西--FragmentStateAdapter惦积,這個(gè)Adapter在為了加載Fragment,花費(fèi)了很多的功夫猛频,為我們想要使用Adapter加載Fragment提供了非常權(quán)威的參考狮崩。

3. ViewPager2的基本分析

??從這里開始,我們正式開始分析源碼伦乔。我們先來看看ViewPager2的基本源碼厉亏,重點(diǎn)在initialize方法里面:

    private void initialize(Context context, AttributeSet attrs) {
        // 初始化RecyclerView
        mRecyclerView = new RecyclerViewImpl(context);
        mRecyclerView.setId(ViewCompat.generateViewId());
        // 初始化LayoutManager
        mLayoutManager = new LinearLayoutManagerImpl(context);
        mRecyclerView.setLayoutManager(mLayoutManager);
        setOrientation(context, attrs);

        mRecyclerView.setLayoutParams(
                new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());

        // 創(chuàng)建滑動(dòng)事件轉(zhuǎn)換器的對象
        mScrollEventAdapter = new ScrollEventAdapter(mLayoutManager);
        // 創(chuàng)建模擬拖動(dòng)事件的對象
        mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
        // 創(chuàng)建PagerSnapHelper對象,用來實(shí)現(xiàn)頁面切換的基本效果
        mPagerSnapHelper = new PagerSnapHelperImpl();
        mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
    
        mRecyclerView.addOnScrollListener(mScrollEventAdapter);
        // ······
    }

??在initialize方法里面烈和,主要初始化RecyclerView的基本配置和基本組件爱只。在這個(gè)方面,做了兩件比較重要的事情:1. 給RecyclerView設(shè)置了滑動(dòng)監(jiān)聽事件招刹,涉及到的組件是ScrollEventAdapter恬试,后面的基本功能都需要這個(gè)組件的支持窝趣;2. 設(shè)置了PagerSnapHelper,目的是實(shí)現(xiàn)切面切換的效果。
??我們對ViewPager2有了基本的了解之后训柴,現(xiàn)在就來對各個(gè)組件進(jìn)行詳細(xì)的分析哑舒。

4. PagerSnapHelper

??在 RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關(guān)組件的源碼分析文章里面,我已經(jīng)簡單分析過SnapHelper幻馁。我們知道SnapHelper最重要的三個(gè)方法是:calculateDistanceToFinalSnap洗鸵、findSnapViewfindTargetSnapPosition
??為了更好區(qū)分這三個(gè)方法的不同點(diǎn)仗嗦,我以一個(gè)非常常用的場景來描述這三個(gè)方法的調(diào)用膘滨,分別分為如下三個(gè)階段:

  1. 假設(shè)手指在快速滑動(dòng)一個(gè)RecyclerView,在手指離開屏幕之前稀拐,如上的三個(gè)方法都不會(huì)被調(diào)用火邓。
  2. 而此時(shí)如果手指如果手指離開了屏幕,接下來就是Fling事件來滑動(dòng)RecyclerView德撬,在Fling事件觸發(fā)之際铲咨,findTargetSnapPosition方法會(huì)被調(diào)用,此方法的作用就是用來計(jì)算Fling事件能滑動(dòng)到位置蜓洪。
  3. 當(dāng)Fling事件結(jié)束之際纤勒,RecyclerView會(huì)回調(diào)SnapHelper內(nèi)部OnScrollListener接口的onScrollStateChanged方法。此時(shí)RecyclerView的滑動(dòng)狀態(tài)為RecyclerView.SCROLL_STATE_IDLE,所以就會(huì)分別調(diào)用findSnapView方法來找到需要顯示在RecyclerView的最前面的View蝠咆。找到目標(biāo)View之后踊东,就會(huì)調(diào)用calculateDistanceToFinalSnap方法來計(jì)算需要滑動(dòng)的距離北滥,然后調(diào)動(dòng)RecyclerView相關(guān)方法進(jìn)行滑動(dòng)刚操。

??正常來說,當(dāng)RecyclerView在Fling時(shí)再芋,如果想要不去攔截Fling時(shí)間,想讓RecyclerView開心的Fling菊霜,可以直接在findTargetSnapPosition方法返回RecyclerView.NO_POSITION即可,從而將Fling事件交給RecyclerView,或者我們可以在findTargetSnapPosition方法來計(jì)算滑動(dòng)的最終位置济赎,然后通過SmoothScroller來實(shí)現(xiàn)滑動(dòng)鉴逞。
??但是,我們知道PagerSnapHelper不支持Fling事件司训,所以在PagerSnapHelper內(nèi)部构捡,必須實(shí)現(xiàn)findTargetSnapPosition方法,從而避免RecyclerViewFling壳猜。

(1). findTargetSnapPosition方法

??熟悉PagerSnapHelper的基本知識(shí)之后勾徽,現(xiàn)在我們來重點(diǎn)分析這三個(gè)方法,我們先來看看findTargetSnapPosition方法统扳,看看它是怎么阻止RecyclerView的Fling事件喘帚。

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        // ······
        // 找到與當(dāng)前View相鄰的View畅姊,包括左相鄰和右響鈴,并且計(jì)算滑動(dòng)的距離
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            if (child == null) {
                continue;
            }
            final int distance = distanceToCenter(layoutManager, child, orientationHelper);

            if (distance <= 0 && distance > distanceBefore) {
                // Child is before the center and closer then the previous best
                distanceBefore = distance;
                closestChildBeforeCenter = child;
            }
            if (distance >= 0 && distance < distanceAfter) {
                // Child is after the center and closer then the previous best
                distanceAfter = distance;
                closestChildAfterCenter = child;
            }
        }

        // 根據(jù)滑動(dòng)的方向來返回的相應(yīng)位置
        final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
        if (forwardDirection && closestChildAfterCenter != null) {
            return layoutManager.getPosition(closestChildAfterCenter);
        } else if (!forwardDirection && closestChildBeforeCenter != null) {
            return layoutManager.getPosition(closestChildBeforeCenter);
        }

        // 兜底計(jì)算
        View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
        if (visibleView == null) {
            return RecyclerView.NO_POSITION;
        }
        int visiblePosition = layoutManager.getPosition(visibleView);
        int snapToPosition = visiblePosition
                + (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);

        if (snapToPosition < 0 || snapToPosition >= itemCount) {
            return RecyclerView.NO_POSITION;
        }
        return snapToPosition;
    }

??從上面的代碼中吹由,我們可以非常容易得到一個(gè)信息咒林,為了阻止RecyclerView的Fling事件顶考,findTargetSnapPosition方法直接返回當(dāng)前ItemView的上一個(gè)ItemView或者下一個(gè)ItemView的位置。所以PagerSnapHelperfindTargetSnapPosition方法還是非常簡單的。
??那么findTargetSnapPosition方法是怎么阻止Fling事件的觸發(fā)呢啸盏?首先得保證findTargetSnapPosition方法返回的值不為RecyclerView.NO_POSITION,然后我們來看看SnapHelpersnapFromFling方法:

    private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return false;
        }

        RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }

        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }

        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

??從snapFromFling方法中我們知道徘公,只要findTargetSnapPosition方法返回不為RecyclerView.NO_POSITION,那么接下來的滑動(dòng)事件會(huì)交給SmoothScroller去處理竣贪,所以RecyclerView最終滑到的位置為當(dāng)前位置的上一個(gè)或者下一個(gè),不會(huì)產(chǎn)生Fling的效果玫荣。

(2). findSnapView方法

??當(dāng)RecyclerView滑動(dòng)完畢之后甚淡,此時(shí)會(huì)先調(diào)用findSnapView方法獲取來最終位置的ItemView。當(dāng)RecyclerView觸發(fā)Fling事件時(shí)捅厂,才會(huì)觸發(fā)findTargetSnapPosition方法贯卦,從而保證RecyclerView滑動(dòng)到正確位置;那么當(dāng)RecyclerView沒有觸發(fā)Fling事件焙贷,怎么保證RecyclerView滑動(dòng)到正確位置呢撵割?當(dāng)然是findSnapView方法和calculateDistanceToFinalSnap方法,這倆方法還有一個(gè)目的就是辙芍,如果Fling沒有滑動(dòng)正確位置啡彬,這倆方法可以做一個(gè)兜底操作:

    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

??在findSnapView內(nèi)部,調(diào)用findCenterView方法故硅,我們先來看看findCenterView方法的代碼:

    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        final int center;
        if (layoutManager.getClipToPadding()) {
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;

        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childCenter = helper.getDecoratedStart(child)
                    + (helper.getDecoratedMeasurement(child) / 2);
            int absDistance = Math.abs(childCenter - center);

            /* if child center is closer than previous closest, set it as closest  */
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }

??findCenterView方法還是比較長庶灿,但是表示的意思非常簡單,就是找到當(dāng)前中心距離屏幕中心最近的ItemView吃衅。這個(gè)怎么來理解呢?比如說往踢,我們手指在滑動(dòng)一個(gè)頁面,滑動(dòng)到一定距離時(shí)就松開了徘层,此時(shí)屏幕當(dāng)中有兩個(gè)頁面峻呕,那么ViewPager2應(yīng)該滑動(dòng)到哪一個(gè)頁面呢?當(dāng)然是距離屏幕中心最近的頁面趣效。findCenterView方法的作用便是如此瘦癌。

(3). calculateDistanceToFinalSnap方法

??找到需要滑到的ItemView,此時(shí)就應(yīng)該調(diào)用calculateDistanceToFinalSnap方法來計(jì)算跷敬,此時(shí)RecyclerView還需要滑動(dòng)多少距離才能達(dá)到正確位置:

    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView) {
        int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

??calculateDistanceToFinalSnap表達(dá)的意思非常簡單讯私,就是計(jì)算RecyclerView需要滑動(dòng)的距離,主要通過distanceToCenter方法來計(jì)算,具體細(xì)節(jié)我們就不討論妄帘,非常簡單楞黄,有興趣的同學(xué)可以去看看。

??我們從整體上了解了PagerSnapHelper的源碼抡驼,應(yīng)該非常容易的知道鬼廓,為什么PagerSnapHelper可以實(shí)現(xiàn)頁面切換的效果。我來簡單的總結(jié)一下:

  1. 首先阻止RecyclerView的Fling事件致盟,阻止的方式就是重寫findTargetSnapPosition方法碎税,當(dāng)RecyclerView觸發(fā)了Fling事件之后,直接滑動(dòng)到下一個(gè)或者上一個(gè)馏锡。
  2. 如果RecyclerView沒有觸發(fā)Fling事件雷蹂,或者Fling階段未能滑動(dòng)到正確位置,此時(shí)需要findSnapView方法和calculateDistanceToFinalSnap來保證滑動(dòng)到正確的頁面杯道。

5. ScrollEventAdapter

??分析完PagerSnaHelper之后匪煌,我們來看看ScrollEventAdapter。前面我們已經(jīng)說過了党巾,ScrollEventAdapter的作用將RecyclerView的滑動(dòng)事件轉(zhuǎn)為ViewPager2的頁面滑動(dòng)事件萎庭。
??在分析源碼之前,我們先來看看幾個(gè)狀態(tài):

名稱 含義
STATE_IDLE 表示當(dāng)前ViewPager2處于停止?fàn)顟B(tài)
STATE_IN_PROGRESS_MANUAL_DRAG 表示當(dāng)前ViewPager2處于手指拖動(dòng)狀態(tài)
STATE_IN_PROGRESS_SMOOTH_SCROLL 表示當(dāng)前ViewPager2處于緩慢滑動(dòng)的狀態(tài)齿拂。這個(gè)狀態(tài)只在調(diào)用了ViewPager2setCurrentItem方法才有可能出現(xiàn)驳规。
STATE_IN_PROGRESS_IMMEDIATE_SCROLL 表示當(dāng)前ViewPager2處于迅速滑動(dòng)的狀態(tài)。這個(gè)狀態(tài)只在調(diào)用了ViewPager2setCurrentItem方法才有可能出現(xiàn)署海。
STATE_IN_PROGRESS_FAKE_DRAG 表示當(dāng)前ViewPager2未使用手指滑動(dòng)吗购,而是通過FakerDrag實(shí)現(xiàn)的。

??ScrollEventAdapter實(shí)現(xiàn)的是OnScrollListener接口砸狞,所以捻勉,我們的重點(diǎn)放在兩個(gè)實(shí)現(xiàn)方法里面。不過在正式這倆方法之前趾代,我們先來了解幾個(gè)方法贯底,方便后面的理解。

方法名 含義
dispatchStateChanged 將狀態(tài)改變的信息分發(fā)到OnPageChangeCallback監(jiān)聽器撒强,不過需要注意的是:當(dāng)ViewPager2處于停止?fàn)顟B(tài),同時(shí)調(diào)用了setCurrentItem方法來立即切換到某一個(gè)頁面(注意笙什,不是緩慢的切換)飘哨,不會(huì)回調(diào)OnPageChangeCallback的方法。
dispatchSelected 分發(fā)選中頁面的信息琐凭。
dispatchScrolled 分發(fā)頁面滑動(dòng)的相關(guān)信息芽隆。

??接下來,我們將正式分析onScrollStateChangedonScrolled

(1). onScrollStateChanged方法

??當(dāng)RecyclerView的滑動(dòng)狀態(tài)發(fā)生變化胚吁,這個(gè)方法就會(huì)被調(diào)用牙躺。這個(gè)方法主要分為3個(gè)階段,分別如下:

  1. 開始拖動(dòng)腕扶,會(huì)調(diào)用startDrag方法表示拖動(dòng)開始孽拷。
  2. 拖動(dòng)手勢的釋放,此時(shí)ViewPager2會(huì)準(zhǔn)備滑動(dòng)到正確的位置半抱。
  3. 滑動(dòng)結(jié)束脓恕,此時(shí)ScrollEventAdapter會(huì)調(diào)用相關(guān)的方法更新狀態(tài)。
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        // 1. 開始拖動(dòng)
        if (mAdapterState != STATE_IN_PROGRESS_MANUAL_DRAG
                && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
            startDrag(false);
            return;
        }
        // 2. 拖動(dòng)手勢的釋放
        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_SETTLING) {
            // Only go through the settling phase if the drag actually moved the page
            if (mScrollHappened) {
                dispatchStateChanged(SCROLL_STATE_SETTLING);
                // Determine target page and dispatch onPageSelected on next scroll event
                mDispatchSelected = true;
            }
            return;
        }
        // 3. 滑動(dòng)結(jié)束
        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_IDLE) {
            boolean dispatchIdle = false;
            updateScrollEventValues();
            // 如果在拖動(dòng)期間為產(chǎn)生移動(dòng)距離
            if (!mScrollHappened) {
                if (mScrollValues.mPosition != RecyclerView.NO_POSITION) {
                    dispatchScrolled(mScrollValues.mPosition, 0f, 0);
                }
                dispatchIdle = true;
            } else if (mScrollValues.mOffsetPx == 0) {
                dispatchIdle = true;
                if (mDragStartPosition != mScrollValues.mPosition) {
                    dispatchSelected(mScrollValues.mPosition);
                }
            }
            if (dispatchIdle) {
                dispatchStateChanged(SCROLL_STATE_IDLE);
                resetState();
            }
        }
    }

??第1步和第2步我們非常的容易理解窿侈,至于第3步我們需要注意如下兩點(diǎn):

  1. dispatchStateChanged方法的調(diào)用時(shí)機(jī):1. 根本沒有滑動(dòng)炼幔,也就是說,onScrolled方法沒有被調(diào)用史简;2. 滑動(dòng)過乃秀,并且在上一次滑動(dòng)中最后一次調(diào)用onScrolled方法的時(shí)候會(huì)被調(diào)用。
  2. dispatchSelected方法的調(diào)用時(shí)機(jī):當(dāng)mOffsetPx為0時(shí)會(huì)被調(diào)用圆兵,mOffsetPx為0表示當(dāng)前ViewPager2根本未滑動(dòng)环形。

(2). onScrolled方法

??在分析這個(gè)方法之前,我們看一下這個(gè)方法的代碼:

    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        mScrollHappened = true;
        // 更新相關(guān)值
        updateScrollEventValues();

        if (mDispatchSelected) {
            // 拖動(dòng)手勢釋放衙傀,ViewPager2正在滑動(dòng)到正確的位置
            mDispatchSelected = false;
            boolean scrollingForward = dy > 0 || (dy == 0 && dx < 0 == isLayoutRTL());
            mTarget = scrollingForward && mScrollValues.mOffsetPx != 0
                    ? mScrollValues.mPosition + 1 : mScrollValues.mPosition;
            if (mDragStartPosition != mTarget) {
                dispatchSelected(mTarget);
            }
        } else if (mAdapterState == STATE_IDLE) {
            // 調(diào)用了setAdapter方法
            dispatchSelected(mScrollValues.mPosition);
        }

        dispatchScrolled(mScrollValues.mPosition, mScrollValues.mOffset, mScrollValues.mOffsetPx);

        // 因?yàn)檎{(diào)用了setCurrentItem(x, false)不會(huì)觸發(fā)IDLE狀態(tài)的產(chǎn)生抬吟,所以需要在這里
        // 調(diào)用dispatchStateChanged方法
        if ((mScrollValues.mPosition == mTarget || mTarget == NO_POSITION)
                && mScrollValues.mOffsetPx == 0 && !(mScrollState == SCROLL_STATE_DRAGGING)) {
            dispatchStateChanged(SCROLL_STATE_IDLE);
            resetState();
        }
    }

??onScrolled方法里面主要做了兩件事:

  1. 調(diào)用updateScrollEventValues方法更新ScrollEventValues里面的值。
  2. 調(diào)用相關(guān)方法统抬,更新狀態(tài)火本。

??關(guān)于更新ScrollEventValues里面的值,具體的細(xì)節(jié)是非常的簡單,這里就不解釋了聪建。我簡單的解釋一下幾個(gè)屬性的含義:

名稱 含義
mPosition 從開始滑動(dòng)到滑動(dòng)結(jié)束钙畔,一直記錄著當(dāng)前滑動(dòng)到的位置。
mOffset 從一個(gè)頁面滑動(dòng)到另一個(gè)頁面金麸,記錄著滑動(dòng)的百分比擎析。
mOffsetPx 記錄著從開始滑動(dòng)的頁面與當(dāng)前狀態(tài)的滑動(dòng)。每次滑動(dòng)結(jié)束之后挥下,會(huì)被重置揍魂。

??其實(shí)總的來說,ScrollEventAdapter的源碼是非常簡單棚瘟,這里稍微復(fù)雜的就是各種狀態(tài)的更新和相關(guān)的方法的回調(diào)现斋。我來簡單的總結(jié)一下:

  1. 當(dāng)調(diào)用ViewPager2setAdapter方法時(shí),此時(shí)應(yīng)該回調(diào)一次dispatchSelected方法。
  2. 當(dāng)調(diào)用setCurrentItem(x, false)方法偎蘸,不會(huì)調(diào)用onScrollStateChanged方法庄蹋,因而不會(huì)產(chǎn)生idle狀態(tài)瞬内,因此,我們需要在onScrolled方法特殊處理(onScrolled方法會(huì)被調(diào)用)限书。
  3. 正常的拖動(dòng)和釋放虫蝶,就是onScrollStateChanged方法和onScrolled方法的正常回調(diào)倦西。

6. PageTransformerAdapter

??PageTransformerAdapter的作用將OnPageChangeCallback的事件轉(zhuǎn)換成為一種特殊的事件能真,什么特殊的事件呢?我以一個(gè)例子來解釋一下:

  1. 假設(shè)ViewPager2此時(shí)從A頁面滑動(dòng)到B頁面调限,并且是從右往左滑動(dòng)舟陆,其中A頁面的變化范圍:[0,-1);B頁面的變化范圍:[1,0)耻矮。
  2. 假設(shè)ViewPager2此時(shí)從B頁面滑動(dòng)到A頁面秦躯,并且是從左往右滑動(dòng),其中A頁面的變化范圍:[-1,0)裆装;B頁面的變化范圍:[0,1)踱承。

??熟悉ViewPager的同學(xué)應(yīng)該都知道,在ViewPager中也有這么一個(gè)東西哨免。這里我們來看一下PageTransformerAdapter是怎么進(jìn)行轉(zhuǎn)換的茎活。
??PageTransformerAdapter實(shí)現(xiàn)于OnPageChangeCallback接口,監(jiān)聽的是ScrollEventAdapter的頁面滑動(dòng)事件琢唾,然后將頁面滑動(dòng)事件轉(zhuǎn)換成為上面特殊的事件载荔,我們來看看具體的實(shí)現(xiàn),真正的實(shí)現(xiàn)在onPageScrolled方法里面:

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mPageTransformer == null) {
            return;
        }

        float transformOffset = -positionOffset;
        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
            View view = mLayoutManager.getChildAt(i);
            if (view == null) {
                throw new IllegalStateException(String.format(Locale.US,
                        "LayoutManager returned a null child at pos %d/%d while transforming pages",
                        i, mLayoutManager.getChildCount()));
            }
            int currPos = mLayoutManager.getPosition(view);
            float viewOffset = transformOffset + (currPos - position);
            mPageTransformer.transformPage(view, viewOffset);
        }
    }

??相信不用我解釋上面的代碼吧采桃,大家應(yīng)該都能看懂是怎么實(shí)現(xiàn)的懒熙。

7. FragmentStateAdapter

??接下來,我們將分析FragmentStateAdapter普办,看看它是加載Fragment的工扎。在正式分析源碼之前,我們先來幾個(gè)成員變量衔蹲。

變量名稱 變量類型 含義
mFragments LongSparseArray<Fragment> key為itemId肢娘,value為Fragment。表示position與所放Fragment的對應(yīng)關(guān)系(itemId與position有對應(yīng)關(guān)系)
mSavedStates LongSparseArray<Fragment.SavedState> key為itemId舆驶,value為Fragment的狀態(tài)
mItemIdToViewHolder LongSparseArray<Integer> key為itemId, value為ItemView的id橱健。

??接下來,我們將分析在Adapter中比較重要的幾個(gè)方法:

  1. onCreateViewHolder
  2. onBindViewHolder
  3. onViewAttachedToWindow
  4. onViewRecycled
  5. onFailedToRecycleView

??如上5個(gè)方法都與Fragment加載息息相關(guān)贞远,我們一個(gè)一個(gè)的來看畴博。

(1). onCreateViewHolder方法

??onCreateViewHolder方法主要?jiǎng)?chuàng)建ViewHolder,我們來簡單看看怎么創(chuàng)建ViewHolder

    @NonNull
    @Override
    public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return FragmentViewHolder.create(parent);
    }

??其實(shí)就是調(diào)用了FragmentViewHolder的一個(gè)靜態(tài)方法蓝仲,具體細(xì)節(jié)這里就不展示了。

(2). onBindViewHolder方法

??onBindViewHolder方法主要是將Fragment加載到ItemView上,但是由于ViewHolder會(huì)被復(fù)用袱结,所以這里需要很多的條件亮隙。我們先來簡單的看一下代碼:

    public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
        final long itemId = holder.getItemId();
        final int viewHolderId = holder.getContainer().getId();
        final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
        // 如果當(dāng)前ItemView已經(jīng)加載了Fragment,并且不是同一個(gè)Fragment
        // 那么就移除
        if (boundItemId != null && boundItemId != itemId) {
            removeFragment(boundItemId);
            mItemIdToViewHolder.remove(boundItemId);
        }

        mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
        // 保證對應(yīng)位置的Fragment已經(jīng)初始化垢夹,并且放在mFragments中
        ensureFragment(position);

        final FrameLayout container = holder.getContainer();
        // 特殊情況溢吻,當(dāng)RecyclerView讓ItemView保持在Window,
        // 但是不在視圖樹中果元。
        if (ViewCompat.isAttachedToWindow(container)) {
            if (container.getParent() != null) {
                throw new IllegalStateException("Design assumption violated.");
            }
            // 當(dāng)ItemView添加在到RecyclerView中才加載Fragment
            container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v, int left, int top, int right, int bottom,
                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
                    if (container.getParent() != null) {
                        container.removeOnLayoutChangeListener(this);
                        // 加載Fragment
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }

        gcFragments();
    }

?? onBindViewHolder方法主要分為三步:

  1. 如果當(dāng)前ItemView上已經(jīng)加載了Fragment促王,并且不是同一個(gè)Fragment(ItemView被復(fù)用了),那么先移除掉ItemView上的Fragment而晒。
  2. 初始化相關(guān)信息蝇狼。
  3. 如果存在特殊情況,會(huì)走特殊情況倡怎。正常來說迅耘,都會(huì)經(jīng)過onAttachToWindow方法來對Fragment進(jìn)行加載。

?? 這其中监署,第三步是尤為重要的颤专,不過這里,我們先分析它钠乏,待會(huì)詳細(xì)的解釋栖秕。

(3). onViewAttachedToWindow方法

??正常來說,ItemView都會(huì)在這個(gè)方法里面對Fragment進(jìn)行加載晓避,我們來看看代碼:

    @Override
    public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
        placeFragmentInViewHolder(holder);
        gcFragments();
    }

??同樣的簇捍,調(diào)用了placeFragmentInViewHolder方法加載Fragment。

(4). onViewRecycled方法

??當(dāng)ViewHolder被回收到回收池中够滑,onViewRecycled方法會(huì)被調(diào)用垦写。而在onViewRecycled方法里面,自然是對Fragment的卸載彰触。我們簡單的看一下代碼:

    @Override
    public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
        final int viewHolderId = holder.getContainer().getId();
        final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
        if (boundItemId != null) {
            removeFragment(boundItemId);
            mItemIdToViewHolder.remove(boundItemId);
        }
    }

??有人在問梯投,為什么要在onViewRecycled方法來對Fragment進(jìn)行卸載,而不在onViewDetachedFromWindow方法進(jìn)行卸載况毅。
??我們先來分析下onViewRecycled方法分蓖,當(dāng)onViewRecycled方法被調(diào)用,表示當(dāng)前ViewHolder已經(jīng)徹底沒有用了尔许,被放入回收池么鹤,等待后面被復(fù)用,此時(shí)存在的情況可能有:1.當(dāng)前ItemView手動(dòng)移除掉了味廊;2. 當(dāng)前位置對應(yīng)的視圖已經(jīng)徹底不在屏幕中蒸甜,被當(dāng)前屏幕中某些位置復(fù)用了棠耕。所以在onViewRecycled方法里面移除Fragment比較合適。
??那么為什么在onViewDetachedFromWindow方法里面不合適呢柠新?因?yàn)槊慨?dāng)一個(gè)頁面被滑走窍荧,都會(huì)調(diào)用這個(gè)方法,如果對其Fragment進(jìn)行卸載恨憎,此時(shí)用戶又滑回來蕊退,又要重新加載一次,這性能就下降了很多憔恳。
??onFailedToRecycleView方法與onViewRecycled方法操作差不多瓤荔,這里就不過多分析了。

(5). placeFragmentInViewHolder方法

??接下來我們來分析placeFragmentInViewHolder方法钥组,看看怎么加載Fragment输硝。整個(gè)PageTransformerAdapter的核心點(diǎn)就在這個(gè)方法里面。
??在加載Fragment之前者铜,我們需要判斷幾個(gè)狀態(tài):

  1. Fragment是否添加到ItemView 中腔丧。
  2. Fragment的View是否已經(jīng)創(chuàng)建。
  3. Fragment的View 是否添加視圖樹中

??計(jì)算下來作烟,一共8種情況愉粤,我們來看看代碼:

 void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {

        // ······
        // 1.Fragment未添加到ItemView中,但是View已經(jīng)創(chuàng)建
        // 非法狀態(tài)
        if (!fragment.isAdded() && view != null) {
            throw new IllegalStateException("Design assumption violated.");
        }

        // 2.Fragment添加到ItemView中拿撩,但是View未創(chuàng)建
        // 先等待View創(chuàng)建完成衣厘,然后將View添加到Container。
        if (fragment.isAdded() && view == null) {
            scheduleViewAttach(fragment, container);
            return;
        }

        // 3.Fragment添加到ItemView中压恒,同時(shí)View已經(jīng)創(chuàng)建完成并且添加到Container中
        // 需要保證View添加到正確的Container中影暴。
        if (fragment.isAdded() && view.getParent() != null) {
            if (view.getParent() != container) {
                addViewToContainer(view, container);
            }
            return;
        }

        // 4.Fragment添加到ItemView中,同時(shí)View已經(jīng)創(chuàng)建完成但是未添加到Container中
        // 需要將View添加到Container中探赫。
        if (fragment.isAdded()) {
            addViewToContainer(view, container);
            return;
        }

        // 5.Fragment未創(chuàng)建型宙,View未創(chuàng)建、未添加
        if (!shouldDelayFragmentTransactions()) {
            scheduleViewAttach(fragment, container);
            mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();
        } else {
            // 調(diào)用了第5步伦吠,但是Fragment還未真正創(chuàng)建
            if (mFragmentManager.isDestroyed()) {
                return; // nothing we can do
            }
            mLifecycle.addObserver(new GenericLifecycleObserver() {
                @Override
                public void onStateChanged(@NonNull LifecycleOwner source,
                        @NonNull Lifecycle.Event event) {
                    if (shouldDelayFragmentTransactions()) {
                        return;
                    }
                    source.getLifecycle().removeObserver(this);
                    if (ViewCompat.isAttachedToWindow(holder.getContainer())) {
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }
    }

??如上便是加載Fragment所有流程妆兑,還是挺簡單的,就是情況太多了毛仪。由于代碼中的注釋已經(jīng)詳細(xì)解釋了每一步的含義搁嗓,所以這里就不再贅述了。

8. 總結(jié)

??其實(shí)ViewPager2本身的源碼是非常簡單的箱靴,它的核心點(diǎn)就在各個(gè)組件當(dāng)中腺逛,所以本文就不對ViewPager2的內(nèi)部源碼進(jìn)行分析。到此為止衡怀,我們對ViewPager2的源碼分析完畢棍矛,在這里安疗,我在做一個(gè)小小的總結(jié)。

  1. ViewPager2本身是一個(gè)ViewGroup茄靠,沒有特殊作用茂契,只是用來裝一個(gè)RecyclerView蝶桶。
  2. PagerSnapHelper實(shí)現(xiàn)頁面切換效果的原因是calculateDistanceToFinalSnap阻止RecyclerView的Fling事件慨绳,直接讓它滑動(dòng)相鄰頁面;findSnapView方法和findTargetSnapPosition用來輔助滑動(dòng)到正確的位置真竖。
  3. ScrollEventAdapter的作用將RecyclerView的滑動(dòng)事件轉(zhuǎn)換成為ViewPager2的頁面滑動(dòng)事件脐雪。
  4. PageTransformerAdapter的作用將普通的頁面滑動(dòng)事件轉(zhuǎn)換為特殊事件。
  5. FragmentStateAdapter完美實(shí)現(xiàn)了使用Adapter加載Fragment恢共。在FragmentStateAdapter中战秋,完美地考慮到ViewHolder的復(fù)用,F(xiàn)ragment加載和卸載讨韭。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脂信,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子透硝,更是在濱河造成了極大的恐慌狰闪,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件濒生,死亡現(xiàn)場離奇詭異埋泵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)罪治,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門丽声,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人觉义,你說我怎么就攤上這事雁社。” “怎么了晒骇?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵霉撵,是天一觀的道長。 經(jīng)常有香客問我厉碟,道長喊巍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任箍鼓,我火速辦了婚禮崭参,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘款咖。我一直安慰自己何暮,他們只是感情好奄喂,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著海洼,像睡著了一般跨新。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坏逢,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天域帐,我揣著相機(jī)與錄音,去河邊找鬼是整。 笑死肖揣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浮入。 我是一名探鬼主播龙优,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼事秀!你這毒婦竟也來了彤断?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤易迹,失蹤者是張志新(化名)和其女友劉穎宰衙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赴蝇,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菩浙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了句伶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劲蜻。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖考余,靈堂內(nèi)的尸體忽然破棺而出先嬉,到底是詐尸還是另有隱情,我是刑警寧澤楚堤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布疫蔓,位于F島的核電站,受9級特大地震影響身冬,放射性物質(zhì)發(fā)生泄漏衅胀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一酥筝、第九天 我趴在偏房一處隱蔽的房頂上張望滚躯。 院中可真熱鬧,春花似錦、人聲如沸掸掏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丧凤。三九已至募闲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愿待,已是汗流浹背浩螺。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呼盆,地道東北人年扩。 一個(gè)月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像访圃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子相嵌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容