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

  ViewPager2是Google爸爸在幾個月前推出來的新控件,此控件的目的就是爲了替代傳統的ViewPager控件。至於爲何要淘汰ViewPager,我想就不用解釋這其中的緣由吧,ViewPager從來最大的詬病就是不會複用View(其實我對ViewPager的原理了解的很少,各位大佬就當我信口雌黃吧😂😂。)。而ViewPager2內部是經過RecyclerView來實現的,性能固然無可置疑。還有最重要的一點,ViewPager2幾乎複製了ViewPager全部的API,因此,ViewPager2在使用上幾乎跟ViewPage徹底同樣。   本文打算從源碼角度入手,詳細的分析ViewPager2的實現原理。其實早在RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析文章中,我在分析SnapHelper源碼時,在文章裏面簡單的說了一句。而此文算是兌現當初的一個承諾,看看怎麼經過RecyclerView + SnapHelper的方式來實現一個ViewPager。   須要注意的是:目前ViewPager2還不太穩定,因此請謹慎使用到生產環境中。   在閱讀本文以前,建議你們先了解SnapHelper的原理,本文參考文章:bash

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

  注意,本文ViewPager2版本均爲1.0.0-alpha04app

1. 概述

  我在閱讀ViewPager2的源碼以前,思考過一個問題,到底應不該該看看ViewPager2的源碼嗎?其實從簡單的方面來講,真的不必去閱讀它的源碼,熟悉RecyclerView的同窗,ViewPager2內部確定是使用SnapHelper實現。因此,咱們閱讀ViewPager2的源碼究竟是爲了什麼?就是由於閒的蛋疼,而後寫出來裝逼嗎?我想確定不是,我總結以下幾點:ide

  1. 瞭解ViewPager2是怎麼將RecyclerView的滑動事件轉變爲ViewPager的頁面滑動事件。
  2. 瞭解怎麼使用RecyclerView來加載Fragment。

  這其中,我以爲第2點很是的重要,爲何重要呢?RecyclerView加載Fragment這裏涉及到細節很是的多,由於Fragment自己有生命週期,因此咱們如何經過Adapter來有效維護Fragment的生命週期,這自己就是一種挑戰。   本文打算從以下幾個方面來介紹:源碼分析

  1. PagerSnapHelper的源碼分析,主要是瞭解它內部的原理,是如何實現ViewPager的效果。
  2. 各類組件的分析,包括ScrollEventAdapterPageTransformerAdapter
  3. FragmentStateAdapter的源碼分析,主要是瞭解Adapter是怎麼加載Fragment的。

  接下來,咱們正式來分析ViewPager2的源碼分析。性能

2. ViewPager2的基本結構

  在分析ViewPager2源碼以前,咱們先來看看ViewPager的內部結構,瞭解一下ViewPager2是怎麼實現的。   從ViewPager2的源碼中咱們知道,ViewPager2繼承於ViewGroup,其內部包含有一個RecyclerView控件,其餘部分都是圍繞着這個RecyclerView來實現的。總之,ViewPager2是以一個組合的方式來實現的。   這其中,ScrollEventAdapter的做用是將RecyclerView.OnScrollListener事件轉變爲ViewPager2.OnPageChangeCallback事件;FakeDrag的做用是用來實現模擬拖動的效果;PageTransformerAdapter的做用是將頁面的滑動事件轉變爲比率變化,好比說,一個頁面從左到右滑動,變化規則是從0~1,關於這個組件,我相信熟悉ViewPager2的同窗都應該都知道。   最後就是最重要的東西--FragmentStateAdapter,這個Adapter在爲了加載Fragment,花費了不少的功夫,爲咱們想要使用Adapter加載Fragment提供了很是權威的參考。ui

3. ViewPager2的基本分析

  從這裏開始,咱們正式開始分析源碼。咱們先來看看ViewPager2的基本源碼,重點在initialize方法裏面:this

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

        // 建立滑動事件轉換器的對象
        mScrollEventAdapter = new ScrollEventAdapter(mLayoutManager);
        // 建立模擬拖動事件的對象
        mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
        // 建立PagerSnapHelper對象,用來實現頁面切換的基本效果
        mPagerSnapHelper = new PagerSnapHelperImpl();
        mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
    
        mRecyclerView.addOnScrollListener(mScrollEventAdapter);
        // ······
    }
複製代碼

  在initialize方法裏面,主要初始化RecyclerView的基本配置和基本組件。在這個方面,作了兩件比較重要的事情:1. 給RecyclerView設置了滑動監聽事件,涉及到的組件是ScrollEventAdapter,後面的基本功能都須要這個組件的支持;2. 設置了PagerSnapHelper,目的是實現切面切換的效果。   咱們對ViewPager2有了基本的瞭解以後,如今就來對各個組件進行詳細的分析。spa

4. PagerSnapHelper

  在 RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析文章裏面,我已經簡單分析過SnapHelper。咱們知道SnapHelper最重要的三個方法是:calculateDistanceToFinalSnapfindSnapViewfindTargetSnapPosition。   爲了更好區分這三個方法的不一樣點,我以一個很是經常使用的場景來描述這三個方法的調用,分別分爲以下三個階段:code

  1. 假設手指在快速滑動一個RecyclerView,在手指離開屏幕以前,如上的三個方法都不會被調用。
  2. 而此時若是手指若是手指離開了屏幕,接下來就是Fling事件來滑動RecyclerView,在Fling事件觸發之際,findTargetSnapPosition方法會被調用,此方法的做用就是用來計算Fling事件能滑動到位置。
  3. 當Fling事件結束之際,RecyclerView會回調SnapHelper內部OnScrollListener接口的onScrollStateChanged方法。此時RecyclerView的滑動狀態爲RecyclerView.SCROLL_STATE_IDLE,因此就會分別調用findSnapView方法來找到須要顯示在RecyclerView的最前面的View。找到目標View以後,就會調用calculateDistanceToFinalSnap方法來計算須要滑動的距離,而後調動RecyclerView相關方法進行滑動。

  正常來講,當RecyclerView在Fling時,若是想要不去攔截Fling時間,想讓RecyclerView開心的Fling,能夠直接在findTargetSnapPosition方法返回RecyclerView.NO_POSITION便可,從而將Fling事件交給RecyclerView,或者咱們能夠在findTargetSnapPosition方法來計算滑動的最終位置,而後經過SmoothScroller來實現滑動。   可是,咱們知道PagerSnapHelper不支持Fling事件,因此在PagerSnapHelper內部,必須實現findTargetSnapPosition方法,從而避免RecyclerViewFling。orm

(1). findTargetSnapPosition方法

  熟悉PagerSnapHelper的基本知識以後,如今咱們來重點分析這三個方法,咱們先來看看findTargetSnapPosition方法,看看它是怎麼阻止RecyclerView的Fling事件。

@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        // ······
        // 找到與當前View相鄰的View,包括左相鄰和右響鈴,而且計算滑動的距離
        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;
            }
        }

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

        // 兜底計算
        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;
    }
複製代碼

  從上面的代碼中,咱們能夠很是容易獲得一個信息,爲了阻止RecyclerView的Fling事件,findTargetSnapPosition方法直接返回當前ItemView的上一個ItemView或者下一個ItemView的位置。因此PagerSnapHelperfindTargetSnapPosition方法仍是很是簡單的。   那麼findTargetSnapPosition方法是怎麼阻止Fling事件的觸發呢?首先得保證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,那麼接下來的滑動事件會交給SmoothScroller去處理,因此RecyclerView最終滑到的位置爲當前位置的上一個或者下一個,不會產生Fling的效果。

(2). findSnapView方法

  當RecyclerView滑動完畢以後,此時會先調用findSnapView方法獲取來最終位置的ItemView。當RecyclerView觸發Fling事件時,纔會觸發findTargetSnapPosition方法,從而保證RecyclerView滑動到正確位置;那麼當RecyclerView沒有觸發Fling事件,怎麼保證RecyclerView滑動到正確位置呢?固然是findSnapView方法和calculateDistanceToFinalSnap方法,這倆方法還有一個目的就是,若是Fling沒有滑動正確位置,這倆方法能夠作一個兜底操做:

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內部,調用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方法仍是比較長,可是表示的意思很是簡單,就是找到當前中心距離屏幕中心最近的ItemView。這個怎麼來理解呢?好比說,咱們手指在滑動一個頁面,滑動到必定距離時就鬆開了,此時屏幕當中有兩個頁面,那麼ViewPager2應該滑動到哪個頁面呢?固然是距離屏幕中心最近的頁面。findCenterView方法的做用即是如此。

(3). calculateDistanceToFinalSnap方法

  找到須要滑到的ItemView,此時就應該調用calculateDistanceToFinalSnap方法來計算,此時RecyclerView還須要滑動多少距離才能達到正確位置:

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表達的意思很是簡單,就是計算RecyclerView須要滑動的距離,主要經過distanceToCenter方法來計算,具體細節咱們就不討論,很是簡單,有興趣的同窗能夠去看看。

  咱們從總體上了解了PagerSnapHelper的源碼,應該很是容易的知道,爲何PagerSnapHelper能夠實現頁面切換的效果。我來簡單的總結一下:

  1. 首先阻止RecyclerView的Fling事件,阻止的方式就是重寫findTargetSnapPosition方法,當RecyclerView觸發了Fling事件以後,直接滑動到下一個或者上一個。
  2. 若是RecyclerView沒有觸發Fling事件,或者Fling階段未能滑動到正確位置,此時須要findSnapView方法和calculateDistanceToFinalSnap來保證滑動到正確的頁面。

5. ScrollEventAdapter

  分析完PagerSnaHelper以後,咱們來看看ScrollEventAdapter。前面咱們已經說過了,ScrollEventAdapter的做用將RecyclerView的滑動事件轉爲ViewPager2的頁面滑動事件。   在分析源碼以前,咱們先來看看幾個狀態:

名稱 含義
STATE_IDLE 表示當前ViewPager2處於中止狀態
STATE_IN_PROGRESS_MANUAL_DRAG 表示當前ViewPager2處於手指拖動狀態
STATE_IN_PROGRESS_SMOOTH_SCROLL 表示當前ViewPager2處於緩慢滑動的狀態。這個狀態只在調用了ViewPager2setCurrentItem方法纔有可能出現。
STATE_IN_PROGRESS_IMMEDIATE_SCROLL 表示當前ViewPager2處於迅速滑動的狀態。這個狀態只在調用了ViewPager2setCurrentItem方法纔有可能出現。
STATE_IN_PROGRESS_FAKE_DRAG 表示當前ViewPager2未使用手指滑動,而是經過FakerDrag實現的。

  ScrollEventAdapter實現的是OnScrollListener接口,因此,咱們的重點放在兩個實現方法裏面。不過在正式這倆方法以前,咱們先來了解幾個方法,方便後面的理解。

方法名 含義
dispatchStateChanged 將狀態改變的信息分發到OnPageChangeCallback監聽器,不過須要注意的是:ViewPager2處於中止狀態,同時調用了setCurrentItem方法來當即切換到某一個頁面(注意,不是緩慢的切換),不會回調OnPageChangeCallback的方法。
dispatchSelected 分發選中頁面的信息。
dispatchScrolled 分發頁面滑動的相關信息。

  接下來,咱們將正式分析onScrollStateChangedonScrolled

(1). onScrollStateChanged方法

  當RecyclerView的滑動狀態發生變化,這個方法就會被調用。這個方法主要分爲3個階段,分別以下:

  1. 開始拖動,會調用startDrag方法表示拖動開始。
  2. 拖動手勢的釋放,此時ViewPager2會準備滑動到正確的位置。
  3. 滑動結束,此時ScrollEventAdapter會調用相關的方法更新狀態。
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        // 1. 開始拖動
        if (mAdapterState != STATE_IN_PROGRESS_MANUAL_DRAG
                && newState == RecyclerView.SCROLL_STATE_DRAGGING) {
            startDrag(false);
            return;
        }
        // 2. 拖動手勢的釋放
        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. 滑動結束
        if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_IDLE) {
            boolean dispatchIdle = false;
            updateScrollEventValues();
            // 若是在拖動期間爲產生移動距離
            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步咱們須要注意以下兩點:

  1. dispatchStateChanged方法的調用時機:1. 根本沒有滑動,也就是說,onScrolled方法沒有被調用;2. 滑動過,而且在上一次滑動中最後一次調用onScrolled方法的時候會被調用。
  2. dispatchSelected方法的調用時機:當mOffsetPx爲0時會被調用,mOffsetPx爲0表示當前ViewPager2根本未滑動。

(2). onScrolled方法

  在分析這個方法以前,咱們看一下這個方法的代碼:

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

        if (mDispatchSelected) {
            // 拖動手勢釋放,ViewPager2正在滑動到正確的位置
            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) {
            // 調用了setAdapter方法
            dispatchSelected(mScrollValues.mPosition);
        }

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

        // 由於調用了setCurrentItem(x, false)不會觸發IDLE狀態的產生,因此須要在這裏
        // 調用dispatchStateChanged方法
        if ((mScrollValues.mPosition == mTarget || mTarget == NO_POSITION)
                && mScrollValues.mOffsetPx == 0 && !(mScrollState == SCROLL_STATE_DRAGGING)) {
            dispatchStateChanged(SCROLL_STATE_IDLE);
            resetState();
        }
    }
複製代碼

  onScrolled方法裏面主要作了兩件事:

  1. 調用updateScrollEventValues方法更新ScrollEventValues裏面的值。
  2. 調用相關方法,更新狀態。

  關於更新ScrollEventValues裏面的值,具體的細節是很是的簡單,這裏就不解釋了。我簡單的解釋一下幾個屬性的含義:

名稱 含義
mPosition 從開始滑動到滑動結束,一直記錄着當前滑動到的位置。
mOffset 從一個頁面滑動到另外一個頁面,記錄着滑動的百分比。
mOffsetPx 記錄着從開始滑動的頁面與當前狀態的滑動。每次滑動結束以後,會被重置。

  其實總的來講,ScrollEventAdapter的源碼是很是簡單,這裏稍微複雜的就是各類狀態的更新和相關的方法的回調。我來簡單的總結一下:

  1. 當調用ViewPager2setAdapter方法時,此時應該回調一次dispatchSelected方法。
  2. 當調用setCurrentItem(x, false)方法,不會調用onScrollStateChanged方法,於是不會產生idle狀態,所以,咱們須要在onScrolled方法特殊處理(onScrolled方法會被調用)。
  3. 正常的拖動和釋放,就是onScrollStateChanged方法和onScrolled方法的正常回調。

6. PageTransformerAdapter

  PageTransformerAdapter的做用將OnPageChangeCallback的事件轉換成爲一種特殊的事件,什麼特殊的事件呢?我以一個例子來解釋一下:

  1. 假設ViewPager2此時從A頁面滑動到B頁面,而且是從右往左滑動,其中A頁面的變化範圍:[0,-1);B頁面的變化範圍:[1,0)。
  2. 假設ViewPager2此時從B頁面滑動到A頁面,而且是從左往右滑動,其中A頁面的變化範圍:[-1,0);B頁面的變化範圍:[0,1)。

  熟悉ViewPager的同窗應該都知道,在ViewPager中也有這麼一個東西。這裏咱們來看一下PageTransformerAdapter是怎麼進行轉換的。   PageTransformerAdapter實現於OnPageChangeCallback接口,監聽的是ScrollEventAdapter的頁面滑動事件,而後將頁面滑動事件轉換成爲上面特殊的事件,咱們來看看具體的實現,真正的實如今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);
        }
    }
複製代碼

  相信不用我解釋上面的代碼吧,你們應該都能看懂是怎麼實現的。

7. FragmentStateAdapter

  接下來,咱們將分析FragmentStateAdapter,看看它是加載Fragment的。在正式分析源碼以前,咱們先來幾個成員變量。

變量名稱 變量類型 含義
mFragments LongSparseArray key爲itemId,value爲Fragment。表示position與所放Fragment的對應關係(itemId與position有對應關係)
mSavedStates LongSparseArray<Fragment.SavedState> key爲itemId,value爲Fragment的狀態
mItemIdToViewHolder LongSparseArray key爲itemId, value爲ItemView的id。

  接下來,咱們將分析在Adapter中比較重要的幾個方法:

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

  如上5個方法都與Fragment加載息息相關,咱們一個一個的來看。

(1). onCreateViewHolder方法

  onCreateViewHolder方法主要建立ViewHolder,咱們來簡單看看怎麼建立ViewHolder

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

  其實就是調用了FragmentViewHolder的一個靜態方法,具體細節這裏就不展現了。

(2). onBindViewHolder方法

  onBindViewHolder方法主要是將Fragment加載到ItemView上,可是因爲ViewHolder會被複用,因此這裏須要不少的條件。咱們先來簡單的看一下代碼:

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
        // 若是當前ItemView已經加載了Fragment,而且不是同一個Fragment
        // 那麼就移除
        if (boundItemId != null && boundItemId != itemId) {
            removeFragment(boundItemId);
            mItemIdToViewHolder.remove(boundItemId);
        }

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

        final FrameLayout container = holder.getContainer();
        // 特殊狀況,當RecyclerView讓ItemView保持在Window,
        // 可是不在視圖樹中。
        if (ViewCompat.isAttachedToWindow(container)) {
            if (container.getParent() != null) {
                throw new IllegalStateException("Design assumption violated.");
            }
            // 當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. 若是當前ItemView上已經加載了Fragment,而且不是同一個Fragment(ItemView被複用了),那麼先移除掉ItemView上的Fragment。
  2. 初始化相關信息。
  3. 若是存在特殊狀況,會走特殊狀況。正常來講,都會通過onAttachToWindow方法來對Fragment進行加載。

   這其中,第三步是尤其重要的,不過這裏,咱們先分析它,待會詳細的解釋。

(3). onViewAttachedToWindow方法

  正常來講,ItemView都會在這個方法裏面對Fragment進行加載,咱們來看看代碼:

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

  一樣的,調用了placeFragmentInViewHolder方法加載Fragment。

(4). onViewRecycled方法

  當ViewHolder被回收到回收池中,onViewRecycled方法會被調用。而在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進行卸載,而不在onViewDetachedFromWindow方法進行卸載。   咱們先來分析下onViewRecycled方法,當onViewRecycled方法被調用,表示當前ViewHolder已經完全沒有用了,被放入回收池,等待後面被複用,此時存在的狀況可能有:1.當前ItemView手動移除掉了;2. 當前位置對應的視圖已經完全不在屏幕中,被當前屏幕中某些位置複用了。因此在onViewRecycled方法裏面移除Fragment比較合適。   那麼爲何在onViewDetachedFromWindow方法裏面不合適呢?由於每當一個頁面被滑走,都會調用這個方法,若是對其Fragment進行卸載,此時用戶又滑回來,又要從新加載一次,這性能就降低了不少。   onFailedToRecycleView方法與onViewRecycled方法操做差很少,這裏就不過多分析了。

(5). placeFragmentInViewHolder方法

  接下來咱們來分析placeFragmentInViewHolder方法,看看怎麼加載Fragment。整個PageTransformerAdapter的核心點就在這個方法裏面。   在加載Fragment以前,咱們須要判斷幾個狀態:

  1. Fragment是否添加到ItemView 中。
  2. Fragment的View是否已經建立。
  3. Fragment的View 是否添加視圖樹中

  計算下來,一共8種狀況,咱們來看看代碼:

void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {

        // ······
        // 1.Fragment未添加到ItemView中,可是View已經建立
        // 非法狀態
        if (!fragment.isAdded() && view != null) {
            throw new IllegalStateException("Design assumption violated.");
        }

        // 2.Fragment添加到ItemView中,可是View未建立
        // 先等待View建立完成,而後將View添加到Container。
        if (fragment.isAdded() && view == null) {
            scheduleViewAttach(fragment, container);
            return;
        }

        // 3.Fragment添加到ItemView中,同時View已經建立完成而且添加到Container中
        // 須要保證View添加到正確的Container中。
        if (fragment.isAdded() && view.getParent() != null) {
            if (view.getParent() != container) {
                addViewToContainer(view, container);
            }
            return;
        }

        // 4.Fragment添加到ItemView中,同時View已經建立完成可是未添加到Container中
        // 須要將View添加到Container中。
        if (fragment.isAdded()) {
            addViewToContainer(view, container);
            return;
        }

        // 5.Fragment未建立,View未建立、未添加
        if (!shouldDelayFragmentTransactions()) {
            scheduleViewAttach(fragment, container);
            mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();
        } else {
            // 調用了第5步,可是Fragment還未真正建立
            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全部流程,仍是挺簡單的,就是狀況太多了。因爲代碼中的註釋已經詳細解釋了每一步的含義,因此這裏就再也不贅述了。

8. 總結

  其實ViewPager2自己的源碼是很是簡單的,它的核心點就在各個組件當中,因此本文就不對ViewPager2的內部源碼進行分析。到此爲止,咱們對ViewPager2的源碼分析完畢,在這裏,我在作一個小小的總結。

  1. ViewPager2自己是一個ViewGroup,沒有特殊做用,只是用來裝一個RecyclerView
  2. PagerSnapHelper實現頁面切換效果的緣由是calculateDistanceToFinalSnap阻止RecyclerView的Fling事件,直接讓它滑動相鄰頁面;findSnapView方法和findTargetSnapPosition用來輔助滑動到正確的位置。
  3. ScrollEventAdapter的做用將RecyclerView的滑動事件轉換成爲ViewPager2的頁面滑動事件。
  4. PageTransformerAdapter的做用將普通的頁面滑動事件轉換爲特殊事件。
  5. FragmentStateAdapter完美實現了使用Adapter加載Fragment。在FragmentStateAdapter中,完美地考慮到ViewHolder的複用,Fragment加載和卸載。
相關文章
相關標籤/搜索