ViewPager2
是Google爸爸在幾個月前推出來的新控件,此控件的目的就是爲了替代傳統的ViewPager
控件。至於爲何要淘汰ViewPager
,我想就不用解釋這其中的緣由吧,ViewPager
從來最大的詬病就是不會複用View
(其實我對ViewPager
的原理了解的很少,各位大佬就當我信口雌黃吧😂😂。)。而ViewPager2
內部是經過RecyclerView
來實現的,性能固然無可置疑。還有最重要的一點,ViewPager2
幾乎複製了ViewPager
全部的API,因此,ViewPager2
在使用上幾乎跟ViewPage
徹底同樣。 本文打算從源碼角度入手,詳細的分析ViewPager2
的實現原理。其實早在RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析文章中,我在分析SnapHelper
源碼時,在文章裏面簡單的說了一句。而此文算是兌現當初的一個承諾,看看怎麼經過RecyclerView + SnapHelper
的方式來實現一個ViewPager
。 須要注意的是:目前ViewPager2
還不太穩定,因此請謹慎使用到生產環境中。 在閱讀本文以前,建議你們先了解SnapHelper
的原理,本文參考文章:bash
注意,本文ViewPager2
版本均爲1.0.0-alpha04app
我在閱讀ViewPager2
的源碼以前,思考過一個問題,到底應不該該看看ViewPager2
的源碼嗎?其實從簡單的方面來講,真的不必去閱讀它的源碼,熟悉RecyclerView
的同窗,ViewPager2
內部確定是使用SnapHelper
實現。因此,咱們閱讀ViewPager2
的源碼究竟是爲了什麼?就是由於閒的蛋疼,而後寫出來裝逼嗎?我想確定不是,我總結以下幾點:ide
- 瞭解
ViewPager2
是怎麼將RecyclerView
的滑動事件轉變爲ViewPager
的頁面滑動事件。- 瞭解怎麼使用
RecyclerView
來加載Fragment。
這其中,我以爲第2點很是的重要,爲何重要呢?RecyclerView
加載Fragment這裏涉及到細節很是的多,由於Fragment自己有生命週期,因此咱們如何經過Adapter
來有效維護Fragment
的生命週期,這自己就是一種挑戰。 本文打算從以下幾個方面來介紹:源碼分析
PagerSnapHelper
的源碼分析,主要是瞭解它內部的原理,是如何實現ViewPager
的效果。- 各類組件的分析,包括
ScrollEventAdapter
、PageTransformerAdapter
。FragmentStateAdapter
的源碼分析,主要是瞭解Adapter
是怎麼加載Fragment
的。
接下來,咱們正式來分析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
從這裏開始,咱們正式開始分析源碼。咱們先來看看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
在 RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析文章裏面,我已經簡單分析過SnapHelper
。咱們知道SnapHelper
最重要的三個方法是:calculateDistanceToFinalSnap
、findSnapView
和findTargetSnapPosition
。 爲了更好區分這三個方法的不一樣點,我以一個很是經常使用的場景來描述這三個方法的調用,分別分爲以下三個階段:code
- 假設手指在快速滑動一個
RecyclerView
,在手指離開屏幕以前,如上的三個方法都不會被調用。- 而此時若是手指若是手指離開了屏幕,接下來就是Fling事件來滑動
RecyclerView
,在Fling事件觸發之際,findTargetSnapPosition
方法會被調用,此方法的做用就是用來計算Fling事件能滑動到位置。- 當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
方法,從而避免RecyclerView
Fling。orm
熟悉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
的位置。因此PagerSnapHelper
的findTargetSnapPosition
方法仍是很是簡單的。 那麼findTargetSnapPosition
方法是怎麼阻止Fling事件的觸發呢?首先得保證findTargetSnapPosition
方法返回的值不爲RecyclerView.NO_POSITION
,而後咱們來看看SnapHelper
的snapFromFling
方法:
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的效果。
當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
方法的做用即是如此。
找到須要滑到的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
能夠實現頁面切換的效果。我來簡單的總結一下:
- 首先阻止
RecyclerView
的Fling事件,阻止的方式就是重寫findTargetSnapPosition
方法,當RecyclerView
觸發了Fling
事件以後,直接滑動到下一個或者上一個。- 若是
RecyclerView
沒有觸發Fling事件,或者Fling階段未能滑動到正確位置,此時須要findSnapView
方法和calculateDistanceToFinalSnap
來保證滑動到正確的頁面。
分析完PagerSnaHelper
以後,咱們來看看ScrollEventAdapter
。前面咱們已經說過了,ScrollEventAdapter
的做用將RecyclerView
的滑動事件轉爲ViewPager2
的頁面滑動事件。 在分析源碼以前,咱們先來看看幾個狀態:
名稱 | 含義 |
---|---|
STATE_IDLE | 表示當前ViewPager2 處於中止狀態 |
STATE_IN_PROGRESS_MANUAL_DRAG | 表示當前ViewPager2 處於手指拖動狀態 |
STATE_IN_PROGRESS_SMOOTH_SCROLL | 表示當前ViewPager2 處於緩慢滑動的狀態。這個狀態只在調用了ViewPager2 的setCurrentItem 方法纔有可能出現。 |
STATE_IN_PROGRESS_IMMEDIATE_SCROLL | 表示當前ViewPager2 處於迅速滑動的狀態。這個狀態只在調用了ViewPager2 的setCurrentItem 方法纔有可能出現。 |
STATE_IN_PROGRESS_FAKE_DRAG | 表示當前ViewPager2 未使用手指滑動,而是經過FakerDrag 實現的。 |
ScrollEventAdapter
實現的是OnScrollListener
接口,因此,咱們的重點放在兩個實現方法裏面。不過在正式這倆方法以前,咱們先來了解幾個方法,方便後面的理解。
方法名 | 含義 |
---|---|
dispatchStateChanged | 將狀態改變的信息分發到OnPageChangeCallback 監聽器,不過須要注意的是:當ViewPager2 處於中止狀態,同時調用了setCurrentItem方法來當即切換到某一個頁面(注意,不是緩慢的切換),不會回調OnPageChangeCallback 的方法。 |
dispatchSelected | 分發選中頁面的信息。 |
dispatchScrolled | 分發頁面滑動的相關信息。 |
接下來,咱們將正式分析onScrollStateChanged
和onScrolled
。
當RecyclerView
的滑動狀態發生變化,這個方法就會被調用。這個方法主要分爲3個階段,分別以下:
- 開始拖動,會調用
startDrag
方法表示拖動開始。- 拖動手勢的釋放,此時
ViewPager2
會準備滑動到正確的位置。- 滑動結束,此時
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步咱們須要注意以下兩點:
dispatchStateChanged
方法的調用時機:1. 根本沒有滑動,也就是說,onScrolled
方法沒有被調用;2. 滑動過,而且在上一次滑動中最後一次調用onScrolled
方法的時候會被調用。dispatchSelected
方法的調用時機:當mOffsetPx
爲0時會被調用,mOffsetPx
爲0表示當前ViewPager2
根本未滑動。
在分析這個方法以前,咱們看一下這個方法的代碼:
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
方法裏面主要作了兩件事:
- 調用
updateScrollEventValues
方法更新ScrollEventValues
裏面的值。- 調用相關方法,更新狀態。
關於更新ScrollEventValues
裏面的值,具體的細節是很是的簡單,這裏就不解釋了。我簡單的解釋一下幾個屬性的含義:
名稱 | 含義 |
---|---|
mPosition | 從開始滑動到滑動結束,一直記錄着當前滑動到的位置。 |
mOffset | 從一個頁面滑動到另外一個頁面,記錄着滑動的百分比。 |
mOffsetPx | 記錄着從開始滑動的頁面與當前狀態的滑動。每次滑動結束以後,會被重置。 |
其實總的來講,ScrollEventAdapter
的源碼是很是簡單,這裏稍微複雜的就是各類狀態的更新和相關的方法的回調。我來簡單的總結一下:
- 當調用
ViewPager2
的setAdapter
方法時,此時應該回調一次dispatchSelected
方法。- 當調用
setCurrentItem(x, false)
方法,不會調用onScrollStateChanged
方法,於是不會產生idle狀態,所以,咱們須要在onScrolled
方法特殊處理(onScrolled
方法會被調用)。- 正常的拖動和釋放,就是
onScrollStateChanged
方法和onScrolled
方法的正常回調。
PageTransformerAdapter
的做用將OnPageChangeCallback
的事件轉換成爲一種特殊的事件,什麼特殊的事件呢?我以一個例子來解釋一下:
- 假設
ViewPager2
此時從A頁面滑動到B頁面,而且是從右往左滑動,其中A頁面的變化範圍:[0,-1);B頁面的變化範圍:[1,0)。- 假設
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);
}
}
複製代碼
相信不用我解釋上面的代碼吧,你們應該都能看懂是怎麼實現的。
接下來,咱們將分析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中比較重要的幾個方法:
- onCreateViewHolder
- onBindViewHolder
- onViewAttachedToWindow
- onViewRecycled
- onFailedToRecycleView
如上5個方法都與Fragment加載息息相關,咱們一個一個的來看。
onCreateViewHolder
方法主要建立ViewHolder
,咱們來簡單看看怎麼建立ViewHolder
:
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
複製代碼
其實就是調用了FragmentViewHolder
的一個靜態方法,具體細節這裏就不展現了。
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
方法主要分爲三步:
- 若是當前
ItemView
上已經加載了Fragment,而且不是同一個Fragment(ItemView
被複用了),那麼先移除掉ItemView
上的Fragment。- 初始化相關信息。
- 若是存在特殊狀況,會走特殊狀況。正常來講,都會通過
onAttachToWindow
方法來對Fragment進行加載。
這其中,第三步是尤其重要的,不過這裏,咱們先分析它,待會詳細的解釋。
正常來講,ItemView
都會在這個方法裏面對Fragment進行加載,咱們來看看代碼:
@Override
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
複製代碼
一樣的,調用了placeFragmentInViewHolder
方法加載Fragment。
當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
方法操做差很少,這裏就不過多分析了。
接下來咱們來分析placeFragmentInViewHolder
方法,看看怎麼加載Fragment。整個PageTransformerAdapter
的核心點就在這個方法裏面。 在加載Fragment以前,咱們須要判斷幾個狀態:
- Fragment是否添加到ItemView 中。
- Fragment的View是否已經建立。
- 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全部流程,仍是挺簡單的,就是狀況太多了。因爲代碼中的註釋已經詳細解釋了每一步的含義,因此這裏就再也不贅述了。
其實ViewPager2
自己的源碼是很是簡單的,它的核心點就在各個組件當中,因此本文就不對ViewPager2
的內部源碼進行分析。到此爲止,咱們對ViewPager2
的源碼分析完畢,在這裏,我在作一個小小的總結。
ViewPager2
自己是一個ViewGroup
,沒有特殊做用,只是用來裝一個RecyclerView
。PagerSnapHelper
實現頁面切換效果的緣由是calculateDistanceToFinalSnap
阻止RecyclerView
的Fling事件,直接讓它滑動相鄰頁面;findSnapView
方法和findTargetSnapPosition
用來輔助滑動到正確的位置。ScrollEventAdapter
的做用將RecyclerView
的滑動事件轉換成爲ViewPager2
的頁面滑動事件。PageTransformerAdapter
的做用將普通的頁面滑動事件轉換爲特殊事件。FragmentStateAdapter
完美實現了使用Adapter
加載Fragment。在FragmentStateAdapter
中,完美地考慮到ViewHolder
的複用,Fragment加載和卸載。