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加载和卸载。