??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
的原理没酣,本文參考文章:
??注意王财,本文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):
- 了解
ViewPager2
是怎么將RecyclerView
的滑動(dòng)事件轉(zhuǎn)變?yōu)?code>ViewPager的頁面滑動(dòng)事件庆揩。- 了解怎么使用
RecyclerView
來加載Fragment。
??這其中跌穗,我覺得第2點(diǎn)非常的重要订晌,為什么重要呢?RecyclerView
加載Fragment這里涉及到細(xì)節(jié)非常的多,因?yàn)镕ragment本身有生命周期蚌吸,所以我們?nèi)绾瓮ㄟ^Adapter
來有效維護(hù)Fragment
的生命周期锈拨,這本身就是一種挑戰(zhàn)。
??本文打算從如下幾個(gè)方面來介紹:
PagerSnapHelper
的源碼分析羹唠,主要是了解它內(nèi)部的原理奕枢,是如何實(shí)現(xiàn)ViewPager
的效果娄昆。- 各種組件的分析,包括
ScrollEventAdapter
缝彬、PageTransformerAdapter
稿黄。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
洗鸵、findSnapView
和findTargetSnapPosition
。
??為了更好區(qū)分這三個(gè)方法的不同點(diǎn)仗嗦,我以一個(gè)非常常用的場景來描述這三個(gè)方法的調(diào)用膘滨,分別分為如下三個(gè)階段:
- 假設(shè)手指在快速滑動(dòng)一個(gè)
RecyclerView
,在手指離開屏幕之前稀拐,如上的三個(gè)方法都不會(huì)被調(diào)用火邓。- 而此時(shí)如果手指如果手指離開了屏幕,接下來就是Fling事件來滑動(dòng)
RecyclerView
德撬,在Fling事件觸發(fā)之際铲咨,findTargetSnapPosition
方法會(huì)被調(diào)用,此方法的作用就是用來計(jì)算Fling事件能滑動(dòng)到位置蜓洪。- 當(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
方法,從而避免RecyclerView
Fling壳猜。
(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
的位置。所以PagerSnapHelper
的findTargetSnapPosition
方法還是非常簡單的。
??那么findTargetSnapPosition
方法是怎么阻止Fling事件的觸發(fā)呢啸盏?首先得保證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
,那么接下來的滑動(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é)一下:
- 首先阻止
RecyclerView
的Fling事件致盟,阻止的方式就是重寫findTargetSnapPosition
方法碎税,當(dāng)RecyclerView
觸發(fā)了Fling
事件之后,直接滑動(dòng)到下一個(gè)或者上一個(gè)馏锡。- 如果
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)用了ViewPager2 的setCurrentItem 方法才有可能出現(xiàn)驳规。 |
STATE_IN_PROGRESS_IMMEDIATE_SCROLL | 表示當(dāng)前ViewPager2 處于迅速滑動(dòng)的狀態(tài)。這個(gè)狀態(tài)只在調(diào)用了ViewPager2 的setCurrentItem 方法才有可能出現(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)信息芽隆。 |
??接下來,我們將正式分析onScrollStateChanged
和onScrolled
。
(1). onScrollStateChanged方法
??當(dāng)RecyclerView
的滑動(dòng)狀態(tài)發(fā)生變化胚吁,這個(gè)方法就會(huì)被調(diào)用牙躺。這個(gè)方法主要分為3個(gè)階段,分別如下:
- 開始拖動(dòng)腕扶,會(huì)調(diào)用
startDrag
方法表示拖動(dòng)開始孽拷。- 拖動(dòng)手勢的釋放,此時(shí)
ViewPager2
會(huì)準(zhǔn)備滑動(dòng)到正確的位置半抱。- 滑動(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):
dispatchStateChanged
方法的調(diào)用時(shí)機(jī):1. 根本沒有滑動(dòng)炼幔,也就是說,onScrolled
方法沒有被調(diào)用史简;2. 滑動(dòng)過乃秀,并且在上一次滑動(dòng)中最后一次調(diào)用onScrolled
方法的時(shí)候會(huì)被調(diào)用。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
方法里面主要做了兩件事:
- 調(diào)用
updateScrollEventValues
方法更新ScrollEventValues
里面的值。- 調(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é)一下:
- 當(dāng)調(diào)用
ViewPager2
的setAdapter
方法時(shí),此時(shí)應(yīng)該回調(diào)一次dispatchSelected
方法。- 當(dāng)調(diào)用
setCurrentItem(x, false)
方法偎蘸,不會(huì)調(diào)用onScrollStateChanged
方法庄蹋,因而不會(huì)產(chǎn)生idle狀態(tài)瞬内,因此,我們需要在onScrolled
方法特殊處理(onScrolled
方法會(huì)被調(diào)用)限书。- 正常的拖動(dòng)和釋放虫蝶,就是
onScrollStateChanged
方法和onScrolled
方法的正常回調(diào)倦西。
6. PageTransformerAdapter
??PageTransformerAdapter
的作用將OnPageChangeCallback
的事件轉(zhuǎn)換成為一種特殊的事件能真,什么特殊的事件呢?我以一個(gè)例子來解釋一下:
- 假設(shè)
ViewPager2
此時(shí)從A頁面滑動(dòng)到B頁面调限,并且是從右往左滑動(dòng)舟陆,其中A頁面的變化范圍:[0,-1);B頁面的變化范圍:[1,0)耻矮。- 假設(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è)方法:
- onCreateViewHolder
- onBindViewHolder
- onViewAttachedToWindow
- onViewRecycled
- 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
方法主要分為三步:
- 如果當(dāng)前
ItemView
上已經(jīng)加載了Fragment促王,并且不是同一個(gè)Fragment(ItemView
被復(fù)用了),那么先移除掉ItemView
上的Fragment而晒。- 初始化相關(guān)信息蝇狼。
- 如果存在特殊情況,會(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):
- Fragment是否添加到ItemView 中腔丧。
- Fragment的View是否已經(jīng)創(chuàng)建。
- 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é)。
ViewPager2
本身是一個(gè)ViewGroup
茄靠,沒有特殊作用茂契,只是用來裝一個(gè)RecyclerView
蝶桶。PagerSnapHelper
實(shí)現(xiàn)頁面切換效果的原因是calculateDistanceToFinalSnap
阻止RecyclerView
的Fling事件慨绳,直接讓它滑動(dòng)相鄰頁面;findSnapView
方法和findTargetSnapPosition
用來輔助滑動(dòng)到正確的位置真竖。ScrollEventAdapter
的作用將RecyclerView
的滑動(dòng)事件轉(zhuǎn)換成為ViewPager2
的頁面滑動(dòng)事件脐雪。PageTransformerAdapter
的作用將普通的頁面滑動(dòng)事件轉(zhuǎn)換為特殊事件。FragmentStateAdapter
完美實(shí)現(xiàn)了使用Adapter
加載Fragment恢共。在FragmentStateAdapter
中战秋,完美地考慮到ViewHolder
的復(fù)用,F(xiàn)ragment加載和卸載讨韭。