一、ViewPager2介绍
1 简介
ViewPager2是Google 在 androidx 组件包里增加的一个组件,目前已经到了1.0.0-beta02版本。
谷歌为什么要出这个组件呢?官方是这么说的:
ViewPager2 replaces ViewPager, addressing most of its predecessor’s pain-points,
including right-to-left layout support, vertical orientation, modifiable Fragment collections, etc.
2 具体改动:
New features:
支持竖向滚动
完整支持notifyDataSetChanged
能够关闭用户输入 (setUserInputEnabled, isUserInputEnabled)
API changes:
FragmentStateAdapter 替代 FragmentStatePagerAdapter
RecyclerView.Adapter 替代 PagerAdapter
registerOnPageChangeCallback 替代 addPageChangeListener
3 附上官方链接:
官方文档
https://developer.android.google.cn/jetpack/androidx/releases/viewpager2#1.0.0-alpha01
官方Demo
https://github.com/googlesamples/android-viewpager2
二、ViewPager2原理的简单介绍
ViewPager2继承ViewGroup,内部核心是RecycleView加LinearLayoutManager,其实就是对RecycleView封装了一层,所有功能都是围绕着RecyclerView和LinearLayoutManager展开,不过我们在这里不展开介绍了,我们主要关注两个点。
ViewPager2是怎么使用RecyclerView实现ViewPager的效果的?
RecyclerView来怎么配合Fragment的生命周期的
ViewPager2是怎么使用RecyclerView实现ViewPager的效果的
1、我们先从ViewPager2的初始化入手,代码如下:
private void initialize(Context context, AttributeSet attrs) {
mAccessibilityProvider = sFeatureEnhancedA11yEnabled
? new PageAwareAccessibilityProvider()
: new BasicAccessibilityProvider();
//RecyclerView基本设置 begin——————————————————
mRecyclerView = new RecyclerViewImpl(context);
mRecyclerView.setId(ViewCompat.generateViewId());
mLayoutManager = new LinearLayoutManagerImpl(context);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
setOrientation(context, attrs);
mRecyclerView.setLayoutParams(
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
//RecyclerView基本设置 end——————————————————
//创建ScrollEventAdapter,此adapter作用是将RecyclerView.OnScrollListener事件转变为ViewPager2.OnPageChangeCallback事件
mScrollEventAdapter = new ScrollEventAdapter(this);
//创建模拟拖动事件
mFakeDragger = new FakeDrag(this, mScrollEventAdapter, mRecyclerView);
//创建PagerSnapHelper对象,用来拦截fling事件,从而实现ViewPager页面切换的效果
mPagerSnapHelper = new PagerSnapHelperImpl();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
mRecyclerView.addOnScrollListener(mScrollEventAdapter);
mPageChangeEventDispatcher = new CompositeOnPageChangeCallback(3);
mScrollEventAdapter.setOnPageChangeCallback(mPageChangeEventDispatcher);
//pdates mCurrentItem after swipes
final OnPageChangeCallback currentItemUpdater = new OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
if (mCurrentItem != position) {
mCurrentItem = position;
mAccessibilityProvider.onSetNewCurrentItem();
}
}
@Override
public void onPageScrollStateChanged(int newState) {
if (newState == SCROLL_STATE_IDLE) {
updateCurrentItem();
}
}
};
// update internal state firstinternal state first
mPageChangeEventDispatcher.addOnPageChangeCallback(currentItemUpdater);
mAccessibilityProvider.onInitialize(mPageChangeEventDispatcher, mRecyclerView);
mPageChangeEventDispatcher.addOnPageChangeCallback(mExternalPageChangeCallbacks);
// Add mPageTransformerAdapter after mExternalPageChangeCallbacks, because page transform
// events must be fired after scroll events
//PageTransformerAdapter 作用是将页面的滑动事件转变为比率变化,比如,一个页面从左到右滑动,positionOffset变化是从0~1
mPageTransformerAdapter = new PageTransformerAdapter(mLayoutManager);
mPageChangeEventDispatcher.addOnPageChangeCallback(mPageTransformerAdapter);
attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}
在initialize()方法里面,初始化了RecyclerView组件。
主要做了这几件事:
- RecycleView的基本初始化
- 设置了PagerSnapHelper,目的是实现切面切换的效果
- 给RecyclerView设置了滑动监听事件,涉及到的组件是ScrollEventAdapter,后面的基本功能都需要这个组件的支持
ViewPager的效果,就是靠PagerSnapHelper和ScrollEventAdapter实现的。
2、PagerSnapHelper是做什么的?
PagerSnapHelper继承于SnapHelper。SnapHelper顾名思义是Snap+Helper的组合,Snap有移到某位置的含义,Helper为辅助者,
综合场景解释是 将RecyclerView移动到某位置的辅助类。
SnapHelper的内部有两个监听接口:OnFlingListener和OnScrollListener,分别用来监听RecyclerView的fling事件和scroll事件。
SnapHelper有三个重要的方法:
- findTargetSnapPosition(计算Fling事件能滑动到位置)
- findSnapView(找到需要显示在RecyclerView的最前面的View)
- calculateDistanceToFinalSnap(计算需要滑动的距离)。
PagerSnapHelper重写了这三个方法,用以拦截fling事件,做到每次滑动一个item
那么这些方法什么时间调用的?
- 手指在快速滑动,在手指离开屏幕前,三个方法均不调用。
- 手指在快速滑后,手指离开了屏幕,会发生Fling事件,findTargetSnapPosition方法会被调用。
- 当Fling事件结束时,RecyclerView会回调SnapHelper内部OnScrollListener接口的onScrollStateChanged方法。
此时RecyclerView的滑动状态为RecyclerView.SCROLL_STATE_IDLE,会调用findSnapView方法来找到需要
显示在RecyclerView的最前面的View。找到目标View之后,就会调用calculateDistanceToFinalSnap方法来计算需要滑动的距离。然后调用smoothScrollBy滑动到对应位置。
我们先从PagerSnapHelper调用的attachToRecyclerView方法说起。
SnapHelper.java
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
//判断是否是同一个RecyclerView,如果是直接返回,防止重复attach
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
//如果是一个新的RecyclerView,先销毁所有的回调接口,这里指的是
//RecyclerView的scroll监听和fling监听
if (mRecyclerView != null) {
destroyCallbacks();
}
//保存这个RecyclerView
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
//重新设置监听
setupCallbacks();
//初始化一个Scoller,用于滚动RecylerView
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
//移动到指定的已存在的View 后面讲解
snapToTargetExistingView();
}
}
//设置回调关系
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
//注销回调关系
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
SnapHelper在attachToRecyclerView方法中先注册了滚动状态和fling的监听,当监听触发时,如何处理后续的流程?
我们先来看下onScroll事件的处理。
SnapHelper.java
// Handles the snap on scroll case.
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//当newState == 静止状态且滚动距离不等于0,触发snapToTargetExistingView();
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
//移动到指定的已存在的View
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
//查找SnapView
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
//计算SnapView的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
snapToTargetExistingView()这个方法会在第一次attach的时候和RecyclerView滑动状态改变的时候调用,用于将RecyclerView移动到指定的位置,移动的距离根据当前距离RecyclerView中心点最近的那个itemView获取,获取到这个距离后,调用smoothScrollBy方法移动RecyclerView。在这个方法中findSnapView和calculateDistanceToFinalSnap是两个抽象方法。需要子类重写,实现不同的效果。
整理一下滚动状态回调下,SnapHelper的实现流程图如下;
我们接着看下fling事件,
在RecyclerView中fling这里就不展开了。行为流程图如下:
SnapHelper.java
@Override
public boolean onFling(int velocityX, int velocityY) {
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
//获取出发RecyclerView fling的最小速度,这是一个定义好的常量值
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
//当滑动速度满足条件时,执行snapFromFling方法
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
然后我们来看看SnapHelper的snapFromFling方法:
SnapHelper.java
//处理snap的fling逻辑
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判断layoutManager要实现ScrollVectorProvider
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
//创建SmoothScroller
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
//获得snap position
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
//设置position
smoothScroller.setTargetPosition(targetPosition);
//启动SmoothScroll
layoutManager.startSmoothScroll(smoothScroller);
//返回true拦截掉后续的fling操作
return true;
}
@Nullable
@Deprecated
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
if (mRecyclerView == null) {
// The associated RecyclerView has been removed so there is no action to take.
return;
}
//计算Snap到目标位置的距离
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
//计算时间
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
//计算速度
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
fling的逻辑主要在snapFromFling方法中,完成fling逻辑需要几点:
- 要求layoutManager是ScrollVectorProvider的实现
- 创建SmoothScroller,主要逻辑是createSnapScroller方法
- 该方法有默认的实现,主要逻辑是创建一个LinearSmoothScroller
- 通过findTargetSnapPosition方法获取目标targetPosition
- 最后把targetPosition赋值给smoothScroller,通过layoutManager执行该scroller
- snapFromFling要返回true,返回true的话,默认的ViewFlinger就不会执行。
fling流程:
从snapFromFling方法中我们知道,只要findTargetSnapPosition方法返回不为RecyclerView.NO_POSITION,那么接下来的滑动事件会交给SmoothScroller去处理,所以RecyclerView最终滑到的位置为当前位置的上一个或者下一个,不会产生Fling的效果。
为了阻止RecyclerView的Fling事件,findTargetSnapPosition方法需要保证几点
- 返回当前ItemView的上一个ItemView或者下一个ItemView的位置
- 保证findTargetSnapPosition方法返回的值不为RecyclerView.NO_POSITION,
查找指定的SnapPosition
/*
这个方法表示fling操作最终能滑动到I的temView的position。
这个position称为targetSnapPosition,位置上对应的View就是targetSnapView。如果找不到position,就返回RecyclerView.NO_POSITION
*/
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
if (orientationHelper == null) {
return RecyclerView.NO_POSITION;
}
// A child that is exactly in the center is eligible for both before and after
View closestChildBeforeCenter = null;
int distanceBefore = Integer.MIN_VALUE;
View closestChildAfterCenter = null;
int distanceAfter = Integer.MAX_VALUE;
// Find the first view before the center, and the first view after the center
final int childCount = layoutManager.getChildCount();
// 找到与当前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滑动完毕之后,此时会先调用findSnapView方法获取来最终位置的ItemView。当RecyclerView触发Fling事件时,才会触发findTargetSnapPosition方法,从而保证RecyclerView滑动到正确位置;那么当RecyclerView没有触发Fling事件,怎么保证RecyclerView滑动到正确位置呢?当然是findSnapView方法和calculateDistanceToFinalSnap方法,这俩方法还有一个目的就是,如果Fling没有滑动正确位置,这俩方法可以做一个兜底操作。
那么findSnapView和calculateDistanceToFinalSnap究竟怎么做的?
我们先来看PagerSnapHelper重写的findSnapView方法。
PagerSnapHelper.java
@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
//判断RecyclerView的滑动方向,这个可以由layoutManager获取
if (layoutManager.canScrollVertically()) {
//OrientationHelper是对RecycleView中子View管理的工具类
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
/**
* Return the child view that is currently closest to the center of this parent.
* 如注释所说,这个方法返回的是一个距离当前parent也就是RecyclerView中心位置最近的一个item
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
*
* @return the child view that is currently closest to the center of this parent.
*/
@Nullable
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center;
//clipToPadding是RecyclerView的一个属性,表示在含有padding值的时候
//会不会裁剪padding位置的view,如果为true,表示裁剪,那么在padding
//位置滚动的时候是看不到view的
if (layoutManager.getClipToPadding()) {
//getStartAfterPadding 获取RecycleView左侧内边距(paddingLeft)
//getTotalSpace Recycleview水平内容区大小(宽度,除去左右内边距)
//这些方法都会在OrientationHelper中具体说明,如果是Horizontal,
//getStartAfterPadding 得到的是paddingLeft,如果是vertical,那么得到
//的是paddingTop,自己想象一下
//含有padding的时候,RecyclerView的中心位置应该是起始位置的padding
//值加上内容区的宽度的一半,得到的刚好是RecyclerView显示内容的中心
//位置的值
center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
//没有padding,则中心位置的值直接是宽度的一半
center = helper.getEnd() / 2;
}
int absClosest = Integer.MAX_VALUE;
//循环的计算所有childView,得到离上边计算到的中心点值最近的一个
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
//getDecoratedStart返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
//通俗地讲:子View左边界点到父View的(0,0)点的水平间距
///getDecoratedMeasurement返回view在水平方向上所占位置的大小(包括view的左右外边距)
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方法的作用便是如此。
经过上边的计算,就能得到距离当前RecyclerView中心位置最近的一个itemView。
找到需要滑到的ItemView,此时就应该调用calculateDistanceToFinalSnap方法来计算,此时RecyclerView还需要滑动多少距离才能达到正确位置。
@Nullable
@Override
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;
}
private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//getDecoratedStart 返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
//通俗地讲:子View左边界点到父View的(0,0)点的水平间距
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter;
if (layoutManager.getClipToPadding()) {
//获取RecycleView左侧内边距(paddingLeft)
//再次计算RecyclerView中心位置
containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
containerCenter = helper.getEnd() / 2;
}
//用itemView的中心位置减去RecyclerView的中心位置,得到的就是
//将要移动的距离
return childCenter - containerCenter;
}
calculateDistanceToFinalSnap表达的意思非常简单,就是计算RecyclerView需要滑动的距离,主要通过distanceToCenter方法来计算。PagerSnapHelper的移动规则是每次滑动将距离中心位置最近的item移动到中心位置.
3、ScrollEventAdapter转换事件
继承RecyclerView.OnScrollListener,作用是将RecyclerView.OnScrollListener(滑动事件)转变为ViewPager2.OnPageChangeCallback(页面滑动事件)。
ScrollEventAdapter有几种状态值:
名称 | 含义 |
---|---|
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的重心是重写onScrollStateChanged()和onScrolled()
,当RecyclerView的滑动状态发生变化,onScrollStateChanged()方法就会被调用。
onScrollStateChanged()方法
主要有三个步骤:
- 开始拖动,会调用startDrag方法表示拖动开始。
- 拖拽释放,此时ViewPager2会准备滑动到正确的位置。
- 滑动结束,此时ScrollEventAdapter会调用相关的方法更新状态。
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
// 1、用户开始拖拽
if ((mAdapterState != STATE_IN_PROGRESS_MANUAL_DRAG
|| mScrollState != SCROLL_STATE_DRAGGING)
&& newState == RecyclerView.SCROLL_STATE_DRAGGING) {
startDrag(false);
return;
}
// 2、拖拽释放 ,此时ViewPager2会准备滑动到正确的位置
//RecyclerView is snapping to page (dragging -> settling)
// Note that mAdapterState is not updated, to remember we were dragging when settling
if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_SETTLING) {
// Only go through the settling phase if the drag actually moved the page
if (mScrollHappened) {
/**
将状态改变的信息分发到OnPageChangeCallback监听器,不过需要注意的是:当ViewPager2处于停止状态,同时调用了setCurrentItem方法来立即切换到某一个页面(注意,不是缓慢的切换),不会回调OnPageChangeCallback的方法。
*/
dispatchStateChanged(SCROLL_STATE_SETTLING);
// Determine target page and dispatch onPageSelected on next scroll event
mDispatchSelected = true;
}
return;
}
// 3、拖拽结束 此时ScrollEventAdapter会调用相关的方法更新状态 Drag is finished (dragging || settling -> idle)
if (isInAnyDraggingState() && newState == RecyclerView.SCROLL_STATE_IDLE) {
boolean dispatchIdle = false;
updateScrollEventValues();
// 如果在拖动期间为产生移动距离
if (!mScrollHappened) {
// Pages didn't move during drag, so either we're at the start or end of the list,
// or there are no pages at all.
// In the first case, ViewPager's contract requires at least one scroll event.
// In the second case, don't send that scroll event
if (mScrollValues.mPosition != RecyclerView.NO_POSITION) {
dispatchScrolled(mScrollValues.mPosition, 0f, 0);
}
dispatchIdle = true;
} else if (mScrollValues.mOffsetPx == 0) {
// Normally we dispatch the selected page and go to idle in onScrolled when
// mOffsetPx == 0, but in this case the drag was still ongoing when onScrolled was
// called, so that didn't happen. And since mOffsetPx == 0, there will be no further
// scroll events, so fire the onPageSelected event and go to idle now.
// Note that if we _did_ go to idle in that last onScrolled event, this code will
// not be executed because mAdapterState has been reset to STATE_IDLE.
dispatchIdle = true;
if (mDragStartPosition != mScrollValues.mPosition) {
dispatchSelected(mScrollValues.mPosition);
}
}
if (dispatchIdle) {
// Normally idle is fired in last onScrolled call, but either onScrolled was never
// called, or we were still dragging when the last onScrolled was called
dispatchStateChanged(SCROLL_STATE_IDLE);
resetState();
}
}
}
在这个方法中,前两点我们都知道,我们主要看第三个步骤。
dispatchStateChanged方法会在什么时候调用?
- 没有滑动,也就是说,onScrolled方法没有被调用;
- 滑动过,并且在上一次滑动中最后一次调用onScrolled方法的时候会被调用。
dispatchSelected方法会在什么时候调用?
- 当mOffsetPx为0时会被调用,mOffsetPx为0表示当前ViewPager2根本未滑动。
onScrolled()方法
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
mScrollHappened = true;
// 更新相关值
updateScrollEventValues();
if (mDispatchSelected) {
// 拖动手势释放,ViewPager2正在滑动到正确的位置
// Drag started settling, need to calculate target page and dispatch onPageSelected now
mDispatchSelected = false;
boolean scrollingForward = dy > 0 || (dy == 0 && dx < 0 == mViewPager.isRtl());
// "&& values.mOffsetPx != 0": filters special case where we're scrolling forward and
// the first scroll event after settling already got us at the target
mTarget = scrollingForward && mScrollValues.mOffsetPx != 0
? mScrollValues.mPosition + 1 : mScrollValues.mPosition;
if (mDragStartPosition != mTarget) {
dispatchSelected(mTarget);
}
} else if (mAdapterState == STATE_IDLE) {
// onScrolled while IDLE means RV has just been populated after an adapter has been set.
// Contract requires us to fire onPageSelected as well.
int position = mScrollValues.mPosition;
// Contract forbids us to send position = -1 though
// 调用了setAdapter方法
dispatchSelected(position == NO_POSITION ? 0 : position);
}
// If position = -1, there are no items. Contract says to send position = 0 instead.
dispatchScrolled(mScrollValues.mPosition == NO_POSITION ? 0 : mScrollValues.mPosition,
mScrollValues.mOffset, mScrollValues.mOffsetPx);
// Dispatch idle in onScrolled instead of in onScrollStateChanged because RecyclerView
// doesn't send IDLE event when using setCurrentItem(x, false)
// 因为调用了setCurrentItem(x, false)不会触发IDLE状态的产生,所以需要在这里
// 调用dispatchStateChanged方法
if ((mScrollValues.mPosition == mTarget || mTarget == NO_POSITION)
&& mScrollValues.mOffsetPx == 0 && !(mScrollState == SCROLL_STATE_DRAGGING)) {
// When the target page is reached and the user is not dragging anymore, we're settled,
// so go to idle.
// Special case and a bit of a hack when mTarget == NO_POSITION: RecyclerView is being
// initialized and fires a single scroll event. This flags mScrollHappened, so we need
// to reset our state. However, we don't want to dispatch idle. But that won't happen;
// because we were already idle.
dispatchStateChanged(SCROLL_STATE_IDLE);
resetState();
}
}
onScrolled方法里面主要做了两件事:
- 调用updateScrollEventValues方法更新ScrollEventValues里面的值(mPosition,mOffset,mOffsetPx)。
- 调用相关方法,更新状态。
对ScrollEventAdapter总结下:
- 当调用ViewPager2的setAdapter方法时,此时应该回调一次dispatchSelected方法。
- 当调用setCurrentItem(x, false)方法,不会调用onScrollStateChanged方法,因而不会产生idle状态,因此,我们需要在onScrolled方法特殊处理(onScrolled方法会被调用)。
- 正常的拖动和释放,就是onScrollStateChanged方法和onScrolled方法的正常回调。
经过ScrollEventAdapter的转换,将RecyclerView.OnScrollListener(滑动事件)转变为ViewPager2.OnPageChangeCallback(页面滑动事件),那么OnPageChangeCallback怎么处理呢?
ViewPager2中还有一个adapter——PageTransformerAdapter,作用将OnPageChangeCallback的事件转换成为一种特殊的事件,什么特殊的事件呢?
我以一个例子来解释一下:
- 假设ViewPager2此时从A页面滑动到B页面,并且是从右往左滑动,其中A页面的变化范围:[0,-1);B页面的变化范围:[1,0)。
- 假设ViewPager2此时从B页面滑动到A页面,并且是从左往右滑动,其中A页面的变化范围:[-1,0);B页面的变化范围:[0,1)。
@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生命周期
- 变量
//key为itemId,value为Fragment。表示position与所放Fragment的对应关系(itemId与position有对应关系)
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
//key为itemId,value为Fragment的状态
private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();
//key为itemId, value为ItemView的id。
private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
- 方法
- onCreateViewHolder()
- onBindViewHolder()
- onViewAttachedToWindow()
- onViewRecycled()
- onFailedToRecycleView()
(1). onCreateViewHolder方法
onCreateViewHolder方法主要创建ViewHolder,其实就是调用了FragmentViewHolder的一个静态方法.
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
//创建一个宽高都MATCH_PARENT的FrameLayout,注意这里并不像PagerAdapter是Fragment的rootView;
@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}
(2). onBindViewHolder方法
onBindViewHolder方法是将Fragment加载到ItemView上,但是因为RecyclerView的复用机制,所以有很多的条件判断。
我们先看一下代码:
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进行加载。
- 每次调用都会gc一次,主要的避免用户修改数据源造成垃圾对象
(3). onViewAttachedToWindow方法
正常情况下,ItemView会在这个方法里面加载Fragment。
@Override
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
和onBindViewHolder一样,调用了placeFragmentInViewHolder方法加载Fragment。
(4). onViewRecycled方法
当ViewHolder被回收,到回收池中的时候,onViewRecycled方法会被调用。
而在onViewRecycled方法里面,会对Fragment进行remove。
@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进行remove,而不在onViewDetachedFromWindow方法进行remove呢。
当onViewRecycled方法被调用,表示当前ViewHolder已经彻底没有用了,需要被放入回收池,等待被复用。
此时存在的情况可能有:
- 1.当前ItemView手动移除掉了
- 2.当前位置对应的视图已经彻底不在屏幕中,被当前屏幕中某些位置复用了。
所以在onViewRecycled方法里面移除Fragment比较合适。
而onViewDetachedFromWindow方法,每次一个页面被滑走,都会被调用,如果对其Fragment进行卸载,此时又滑回来,又要重新加载一次,这性能就下降了很多。
onFailedToRecycleView方法与onViewRecycled方法差不多,不在介绍。
(5). placeFragmentInViewHolder方法
placeFragmentInViewHolder方法主要是加载Fragment,是整个FragmentStateAdapter的核心点。
在加载Fragment之前,我们需要判断几个状态:
- Fragment是否添加到ItemView 中
- Fragment的View是否已经创建
- Fragment的View 是否添加视图树中
placeFragmentInViewHolder方法中一共有8种情况。
我们来看看代码:
void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
Fragment fragment = mFragments.get(holder.getItemId());
if (fragment == null) {
throw new IllegalStateException("Design assumption violated.");
}
FrameLayout container = holder.getContainer();
View view = fragment.getView();
/*
possible states:
- fragment: { added, notAdded }
- view: { created, notCreated }
- view: { attached, notAttached }
combinations:
- { f:added, v:created, v:attached } -> check if attached to the right container
- { f:added, v:created, v:notAttached} -> attach view to container
- { f:added, v:notCreated, v:attached } -> impossible
- { f:added, v:notCreated, v:notAttached} -> schedule callback for when created
- { f:notAdded, v:created, v:attached } -> illegal state
- { f:notAdded, v:created, v:notAttached } -> illegal state
- { f:notAdded, v:notCreated, v:attached } -> impossible
- { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach
*/
// { f:notAdded, v:created, v:attached } -> illegal state
// { f:notAdded, v:created, v:notAttached } -> illegal state
//——————————————————————————————————————————————————————
// 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()) { // 7
return;
}
source.getLifecycle().removeObserver(this);
if (ViewCompat.isAttachedToWindow(holder.getContainer())) { // 8
placeFragmentInViewHolder(holder);
}
}
});
}
}
- FragmentStateAdapter在遇到预加载时,只会创建Fragment对象,不会把Fragment真正的加入到布局中,所以自带懒加载效果
- FragmentStateAdapter不会一直保留Fragment实例,回收的ItemView也会移除Fragment,所以得做好Fragment`重建后恢复数据的准备
- FragmentStateAdapter在遇到offscreenPageLimit>0时,处理离屏Fragment和可见Fragment没有什么区别,所以无法通过setUserVisibleHint判断显示与否
总结
ViewPager2本身是一个ViewGroup,只是封装一个RecyclerView。
-
PagerSnapHelper实现页面切换效果:
- calculateDistanceToFinalSnap
- findSnapView
- findTargetSnapPosition
ScrollEventAdapter将RecyclerView的滑动事件转换成为ViewPager2的页面滑动事件。
PageTransformerAdapter的作用将普通的页面滑动事件转换为特殊事件。
FragmentStateAdapter中使用Adapter加载Fragment,也考虑到了ViewHolder的复用,Fragment加载和remove。