ViewPager2 使用及源码分析

1.ViewPager2的新特性

简单说下viewPager2的新特征,相信大家都知道基于RecyclerView实现

1.基于RecyclerView实现。这意味着RecyclerView的优点将会被ViewPager2所继承。
2.支持竖直滑动。只需要一个参数就可以改变滑动方向。
3.支持关闭用户输入。通过setUserInputEnabled来设置是否禁止用户滑动页面。
4.支持通过编程方式滚动。通过fakeDragBy(offsetPx)代码模拟用户滑动页面。
5.CompositePageTransformer 支持同时添加多个PageTransformer。
6.支持DiffUtil ,可以添加数据集合改变的item动画。
7.支持RTL (right-to-left)布局。

2.ViewPager2的使用及注意点

使用的话非常简单,跟使用RecyclerView一样~ 下面链接官方文档:
常用方法:
setAdapter() 设置适配器
setOrientation() 设置布局方向
setCurrentItem() 设置当前Item下标
beginFakeDrag() 开始模拟拖拽
fakeDragBy() 模拟拖拽中
endFakeDrag() 模拟拖拽结束
setUserInputEnabled() 设置是否允许用户输入/触摸
setOffscreenPageLimit()设置屏幕外加载页面数量
registerOnPageChangeCallback() 注册页面改变回调
setPageTransformer() 设置页面滑动时的变换效果

(1)关联Tablayout,需要使用TabLayoutMediator在构造方法传入TabLayout和ViewPager
(2)ViewPager2新增了一个fakeDragBy的方法。通过这个方法可以来模拟拖拽。在使用fakeDragBy前需要先beginFakeDrag方法来开启模拟拖拽。
fakeDragBy会返回一个boolean值,true表示有fake drag正在执行,而返回false表示当前没有fake drag在执行。我们通过代码来尝试下:

fun fakeDragBy(view: View) {
    viewPager2.beginFakeDrag()
    if (viewPager2.fakeDragBy(-310f))
        viewPager2.endFakeDrag()
}
需要注意到是fakeDragBy接受一个float的参数,当参数值为正数时表示向前一个页面滑动,当值为负数时表示向下一个页面滑动。 
https://developer.android.com/training/animation/vp2-migration

3.ViewPager2源码分析

看下ViewPager初始化的地方

    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);
        // ······
    }

1.其中mRecyclerView = new RecyclerViewImpl(context);和mLayoutManager = new LinearLayoutManagerImpl(context); 属于重新封装的RecyclerView和LinearLayoutManager,
是分别为了处理事件拦截和ViewPager缓存页面的问题。

2.给RecyclerView设置了滑动监听事件,涉及到的组件是ScrollEventAdapter,后面的基本功能都需要这个组件的支持;
(2) 设置了PagerSnapHelper,目的是实现切面切换的效果

1.PagerSnapHelper分析

这个类主要继承了SnapHelper 然后实现了里面3个重要的方法calculateDistanceToFinalSnap(),findSnapView(),findTargetSnapPosition()

这三个方法在RecyclerView 滑动时调用:下面简单说下什么情况调用:

1.手指在快速滑动一个RecyclerView,在手指离开屏幕之前,如上的三个方法都不会被调用。

2.如果手指如果手指离开了屏幕,接下来就是Fling事件来滑动RecyclerView,在Fling事件触发之际,findTargetSnapPosition方法会被调用,此方法的作用就是用来计算Fling事件能滑动到位置。

image

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;
    }

3.当Fling事件结束之际,RecyclerView会回调SnapHelper内部OnScrollListener接口的onScrollStateChanged方法。此时RecyclerView的滑动状态为RecyclerView.SCROLL_STATE_IDLE,所以就会分别调用findSnapView方法来找到需要显示在RecyclerView的最前面的View。找到目标View之后,就会调用calculateDistanceToFinalSnap方法来计算需要滑动的距离,然后调动RecyclerView相关方法进行滑动。

image

image

当RecyclerView在Fling时,如果想要不去拦截Fling时间,想让RecyclerView开心的Fling,可以直接在findTargetSnapPosition方法返回RecyclerView.NO_POSITION即可,或者我们可以在findTargetSnapPosition方法来计算滑动的最终位置,然后通过SmoothScroller来实现滑动。因为viewPager2是不支持快速滑动Filing事件的,所以在PagerSnapHelper中的findTargetSnapPosition()方法中做了处理下面看看

(1)findTargetSnapPosition() 看看怎么阻止recyclerView的Filing事件
@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;
}

从上面看出为了拦截Filing事件,PagerSnapHelper中的findTargetSnapPosition()方法直接返回当前ItemView的上一个ItemView或者下一个ItemView的位置,只要不返回RecyclerView.NO_POSITION就不会有Fling效果! ——> 怎么阻止的Fling事件的触发呢?看看原因:(1)会调用父类(SnapHelper)里面的snapFromFling方法,只要findTargetSnapPosition方法返回不为RecyclerView.NO_POSITION,就会直接走startSmotthiScroll(),所以RecyclerView最终滑到的位置为当前位置的上一个或者下一个,不会产生Fling的效果。上面贴过snapFromFling()方法的代码

(2)findSnapView() 确定最终位置的ItemView

当recyclerView滑动完之后,就会调用findSnapView这个方法,来确认最终位置的ItemView

    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 = helper.getStartAfterPadding() + helper.getTotalSpace() / 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;
    }

上面代码也简单,找到当前中心距离屏幕中心最近的ItemView。这个怎么来理解呢?比如说,我们手指在滑动一个页面,滑动到一定距离时就松开了,此时屏幕当中有两个页面,那么ViewPager2应该滑动到哪一个页面呢?当然是距离屏幕中心最近的页面。findCenterView方法的作用便是如此。

(3)calculateDistanceToFinalSnap()
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;
}

最终掉到distanceToCenter()

    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        final int childCenter = helper.getDecoratedStart(targetView)
                + (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        return childCenter - containerCenter;
    }

calculateDistanceToFinalSnap()方法就是计算RecyclerView需要滑动的距离,主要通过distanceToCenter方法来计算,确定最终要展示的positon

(4)PagerSnapHelper总结

PagerSnapHelper可以实现页面切换的效果,主要如下几点:

(1)首先阻止RecyclerView的Fling事件,阻止的方式就是重写findTargetSnapPosition方法,当RecyclerView触发了Fling事件之后,直接滑动到下一个或者上一个。

(2)如果RecyclerView没有触发Fling事件,或者Fling阶段未能滑动到正确位置,此时需要findSnapView方法和calculateDistanceToFinalSnap来保证滑动到正确的页面。

2.ScrollEventAdapter分析->将recyclerView的滚动事件转换成viewPager2的事件

  //表示当前ViewPager2处于停止状态
  private static final int STATE_IDLE = 0;
  //表示当前ViewPager2处于手指拖动状态
  private static final int STATE_IN_PROGRESS_MANUAL_DRAG = 1;
  //表示当前ViewPager2处于缓慢滑动的状态。这个状态只在调用了ViewPager2的setCurrentItem方法才有可能出现。
  private static final int STATE_IN_PROGRESS_SMOOTH_SCROLL = 2;
  //表示当前ViewPager2处于迅速滑动的状态。这个状态只在调用了ViewPager2的setCurrentItem方法才有可能出现。
  private static final int STATE_IN_PROGRESS_IMMEDIATE_SCROLL = 3;
  //表示当前ViewPager2未使用手指滑动,而是通过FakerDrag实现的。
  private static final int STATE_IN_PROGRESS_FAKE_DRAG = 4;

ScrollEventAdapter实现了recyclerView的OnScrollListener事件,我们只需要看onScrollStateChanged()和onScolled方法

1.onScrollStateChanged()

当RecyclerView的滑动状态发生变化,这个方法就会被调用

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. 拖动手势的释放
        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);
                mDispatchSelected = true;
            }
            return;
        }
        
        //3.滑动结束
        // Drag is finished (dragging || settling -> idle)
        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();
            }
        }

        if (mAdapterState == STATE_IN_PROGRESS_SMOOTH_SCROLL
                && newState == RecyclerView.SCROLL_STATE_IDLE && mDataSetChangeHappened) {
            updateScrollEventValues();
            if (mScrollValues.mOffsetPx == 0) {
                if (mTarget != mScrollValues.mPosition) {
                    dispatchSelected(
                            mScrollValues.mPosition == NO_POSITION ? 0 : mScrollValues.mPosition);
                }
                dispatchStateChanged(SCROLL_STATE_IDLE);
                resetState();
            }
        }
    }
上面大概分三步:
(1)开始拖动,会调用startDrag方法表示拖动开始。
(2)拖动手势的释放,此时ViewPager2会准备滑动到正确的位置。
(3)滑动结束,此时ScrollEventAdapter会调用相关的方法更新状态。

2.onScrolled()

public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
    mScrollHappened = true;
    // 更新且计算当前position和offset的值
    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里面3个变量,(2)调用相关dispatch...方法更新状态和分发状态下去

    private static final class ScrollEventValues {
        int mPosition;//从开始滑动到滑动结束,一直记录着当前滑动到的位置。
        float mOffset;//从一个页面滑动到另一个页面,记录着滑动的百分比。
        int mOffsetPx;//记录着从开始滑动的页面与当前状态的滑动。每次滑动结束之后,会被重置。
    }

ScrollEventAdapter总结:

(1)当调用ViewPager2的setAdapter方法时,此时应该回调一次dispatchSelected方法。
(2)当调用setCurrentItem(x, false)方法,不会调用onScrollStateChanged方法,于是不会产生idle状态,
   所以,咱们须要在onScrolled方法特殊处理(onScrolled方法会被调用)。
(3)正常的拖动和释放,就是onScrollStateChanged方法和onScrolled方法的正常回调。

3.PageTransformerAdapter

    void setPageTransformer(@Nullable PageTransformer transformer) {
       // TODO: add support for reverseDrawingOrder: b/112892792
       // TODO: add support for pageLayerType: b/112893074
       mPageTransformer = transformer; 
   }
   
    @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);
       }
   }

PageTransformerAdapter实现也非常简单,PageTransformerAdapter实现于OnPageChangeCallback接口,监听的是ScrollEventAdapter的页面滑动事件,然后将页面滑动事件转换成为上面特殊的事件 下面说说viewPager2为我们提供的2个PageTransformer的实现类

(1)MarginPageTransformer

PageTransformer是一个位于ViewPager2中的接口,

image

(1)ViewPager2移除了setPageMargin方法,那么怎么为ViewPager2设置页面间距呢?
ViewPager2中为我们提供了MarginPageTransformer,我们可以通过ViewPager2的setPageTransformer方法来设置页面间距。
viewPager2.setPageTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt())

(2)CompositePageTransformer

ViewPager2设置了页面间距后如果还想设置页面动画的Transformer怎么办呢?这是时候就需要用到CompositePageTransformer,因为CompositePageTransformer实现了PageTransformer接口,同时在其内部维护了一个List集合,我们可以将多个PageTransformer添加到CompositePageTransformer中。

public final class CompositePageTransformer implements PageTransformer {
    private final List<PageTransformer> mTransformers = new ArrayList<>();
    public void addTransformer(@NonNull PageTransformer transformer) {
        mTransformers.add(transformer);
    }

    public void removeTransformer(@NonNull PageTransformer transformer) {
        mTransformers.remove(transformer);
    }

    @Override
    public void transformPage(@NonNull View page, float position) {
        for (PageTransformer transformer : mTransformers) {
            transformer.transformPage(page, position);
        }
    }
}
val compositePageTransformer = CompositePageTransformer()
compositePageTransformer.addTransformer(ScaleInTransformer())
compositePageTransformer.addTransformer(MarginPageTransformer(resources.getDimension(R.dimen.dp_10).toInt()))
viewPager2.setPageTransformer(compositePageTransformer)

上述代码中我们通过CompositePageTransformer为ViewPager设置了MarginPageTransformer和一个页面缩放的ScaleInTransformer。当然还可以定制不同的页面动画只需要实现PageTransformer接口,然后放到一起即可

4.FragmentStateAdapter

下面简单说下fragmentStateAdapter与ViewPager2的配合,看下几个变量含义,然后看adapter的onCreateViewHolder()和onBindViewHolder()

    //(1)key为itemId,value为Fragment。表示position与所放Fragment的对应关系(itemId与position有对应关系)
    final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
    //(2)key为itemId,value为Fragment的状态
    private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();
    //(3)key为itemId, value为ItemView的id。
    private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
    private FragmentMaxLifecycleEnforcer mFragmentMaxLifecycleEnforcer;
    boolean mIsInGracePeriod = false;
    private boolean mHasStaleFragments = false;

(1).onCreateViewHolder()

public final class FragmentViewHolder extends ViewHolder {
    private FragmentViewHolder(@NonNull FrameLayout container) {
        super(container);
    }

    @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);
    }

    @NonNull FrameLayout getContainer() {
        return (FrameLayout) itemView;
    }
}

很简单就是创建FrameLayout视图,就不多说

(2).onBindViewHolder()

    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); 
        // 如果当前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中-->确定相应的Fragment,
        ensureFragment(position);
        final FrameLayout container = holder.getContainer();
        // 特殊情况处理,当RecyclerView让ItemView保持在Window,
        // 但是不在视图树中。
        if (ViewCompat.isAttachedToWindow(container)) {
            if (container.getParent() != null) {
                throw new IllegalStateException("Design assumption violated.");
            }
            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);
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }

        gcFragments();
    }
    
  private void ensureFragment(int position) {
        long itemId = getItemId(position);
        if (!mFragments.containsKey(itemId)) {
            // TODO(133419201): check if a Fragment provided here is a new Fragment
            Fragment newFragment = createFragment(position);
            newFragment.setInitialSavedState(mSavedStates.get(itemId));
            mFragments.put(itemId, newFragment);
        }
    }
(1)如果当前ItemView上已经加载了Fragment,并且不是同一个Fragment(ItemView被复用了),那么先移除掉ItemView上的Fragment。
(2)ensureFragment()初始化Fragment视图  
(3)如果存在特殊情况,会走特殊情况。正常来说,都会经过onAttachToWindow方法来对Fragment进行加载。

(3).onViewAttachedToWindow (ViewHolder与Fragment进行绑定)

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

同样的,调用了placeFragmentInViewHolder方法加载Fragment。placeFragmentInViewHolder这个方法的代码放到最后贴出来说

(4).onViewRecycled (ViewHolder与Fragment解除绑定)

当ViewHolder被回收到回收池中,onViewRecycled方法会被调用,来看看代码

    @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);
        }
    }

这里重要的一点是:没有在onViewDetachedFromWindow中去删除Fragment,而是在onViewRecycled中回收。因为当onViewRecycled方法被调用,表示当前ViewHolder已经彻底没有用了,被放入回收池,等待后面被复用,此时存在的情况可能有:(1).当前ItemView手动移除掉了;(2).当前位置对应的视图已经彻底不在屏幕中,被当前屏幕中某些位置复用了。所以在onViewRecycled方法里面移除Fragment比较合适 2.不在onViewDetachedFromWindow中删除的原因呢? 因为每当一个页面被滑走,都会调用这个方法,如果对其Fragment进行卸载,此时用户又滑回来,又要重新加载一次,这性能就下降了很多

(5).placeFragmentInViewHolder

 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())
                    .setMaxLifecycle(fragment, STARTED)  //懒加载
                    .commitNow();
          mFragmentMaxLifecycleEnforcer.updateFragmentMaxLifecycle(false);
        } 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);
                    }
                }
            });
        }
    }

这里说下viewPager2的Lifecycle跟ViewPager一样默认是(setMaxLifecycle(fragment, STARTED) 懒加载),然后滑动界面时设置为Resume,代码如下

public void onPageScrollStateChanged(int state) {
      updateFragmentMaxLifecycle(false);
}

5.总结

(1)ViewPager2本身是一个ViewGroup,没有特殊作用,只是用来装一个RecyclerView。
(2)PagerSnapHelper实现页面切换效果的原因是calculateDistanceToFinalSnap阻止RecyclerView的Fling事件,直接让它滑动相邻页面;findSnapView方法和findTargetSnapPosition用来辅助滑动到正确的位置。
(3)ScrollEventAdapter的作用将RecyclerView的滑动事件转换成为ViewPager2的页面滑动事件。
(4)PageTransformerAdapter的作用将普通的页面滑动事件转换为特殊事件。
(5)FragmentStateAdapter完美实现了使用Adapter加载Fragment。在FragmentStateAdapter中,完美地考虑到ViewHolder的复用,Fragment加载和卸载。
推荐文章:

【SnapHelper 讲解】,方便理解ViewPager2里的PagerSnapHelper
https://hitendev.github.io/2019/04/24/SnapHelper%E7%A1%AC%E6%A0%B8%E8%AE%B2%E8%A7%A3/

【ViewPager2避坑系列】瞬间暴增数个Fragment::
https://hitendev.github.io/2019/05/19/%E3%80%90ViewPager2%E9%81%BF%E5%9D%91%E7%B3%BB%E5%88%97%E3%80%91%E7%9E%AC%E9%97%B4%E6%9A%B4%E5%A2%9E%E6%95%B0%E4%B8%AAFragment/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容