RecyclerView扩展(五) - ViewPager2的源码分析

  ViewPager2是Google爸爸在几个月前推出来的新控件,此控件的目的就是为了替代传统的ViewPager控件。至于为什么要淘汰ViewPager,我想就不用解释这其中的原因吧,ViewPager历来最大的诟病就是不会复用View(其实我对ViewPager的原理了解的不多,各位大佬就当我信口雌黄吧😂😂。)。而ViewPager2内部是通过RecyclerView来实现的,性能当然不容置疑。还有最重要的一点,ViewPager2几乎复制了ViewPager所有的API,所以,ViewPager2在使用上几乎跟ViewPage完全一样。
  本文打算从源码角度入手,详细的分析ViewPager2的实现原理。其实早在RecyclerView 源码分析(七) - 自定义LayoutManager及其相关组件的源码分析文章中,我在分析SnapHelper源码时,在文章里面简单的说了一句。而此文算是兑现当初的一个承诺,看看怎么通过RecyclerView + SnapHelper的方式来实现一个ViewPager
  需要注意的是:目前ViewPager2还不太稳定,所以请谨慎使用到生产环境中。
  在阅读本文之前,建议大家先了解SnapHelper的原理,本文参考文章:

  1. RecyclerView 源码分析(七) - 自定义LayoutManager及其相关组件的源码分析

  注意,本文ViewPager2版本均为1.0.0-alpha04

1. 概述

  我在阅读ViewPager2的源码之前,思考过一个问题,到底应不应该看看ViewPager2的源码吗?其实从简单的方面来说,真的没必要去阅读它的源码,熟悉RecyclerView的同学,ViewPager2内部肯定是使用SnapHelper实现。所以,我们阅读ViewPager2的源码到底是为了什么?就是因为闲的蛋疼,然后写出来装逼吗?我想肯定不是,我总结如下几点:

  1. 了解ViewPager2是怎么将RecyclerView的滑动事件转变为ViewPager的页面滑动事件。
  2. 了解怎么使用RecyclerView来加载Fragment。

  这其中,我觉得第2点非常的重要,为什么重要呢?RecyclerView加载Fragment这里涉及到细节非常的多,因为Fragment本身有生命周期,所以我们如何通过Adapter来有效维护Fragment的生命周期,这本身就是一种挑战。
  本文打算从如下几个方面来介绍:

  1. PagerSnapHelper的源码分析,主要是了解它内部的原理,是如何实现ViewPager的效果。
  2. 各种组件的分析,包括ScrollEventAdapterPageTransformerAdapter
  3. FragmentStateAdapter的源码分析,主要是了解Adapter是怎么加载Fragment的。

  接下来,我们正式来分析ViewPager2的源码分析。

2. 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提供了非常权威的参考。

3. ViewPager2的基本分析

  从这里开始,我们正式开始分析源码。我们先来看看ViewPager2的基本源码,重点在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());

        // 创建滑动事件转换器的对象
        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有了基本的了解之后,现在就来对各个组件进行详细的分析。

4. PagerSnapHelper

  在 RecyclerView 源码分析(七) - 自定义LayoutManager及其相关组件的源码分析文章里面,我已经简单分析过SnapHelper。我们知道SnapHelper最重要的三个方法是:calculateDistanceToFinalSnapfindSnapViewfindTargetSnapPosition
  为了更好区分这三个方法的不同点,我以一个非常常用的场景来描述这三个方法的调用,分别分为如下三个阶段:

  1. 假设手指在快速滑动一个RecyclerView,在手指离开屏幕之前,如上的三个方法都不会被调用。
  2. 而此时如果手指如果手指离开了屏幕,接下来就是Fling事件来滑动RecyclerView,在Fling事件触发之际,findTargetSnapPosition方法会被调用,此方法的作用就是用来计算Fling事件能滑动到位置。
  3. 当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方法,从而避免RecyclerViewFling。

(1). findTargetSnapPosition方法

  熟悉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的位置。所以PagerSnapHelperfindTargetSnapPosition方法还是非常简单的。
  那么findTargetSnapPosition方法是怎么阻止Fling事件的触发呢?首先得保证findTargetSnapPosition方法返回的值不为RecyclerView.NO_POSITION,然后我们来看看SnapHelpersnapFromFling方法:

    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的效果。

(2). findSnapView方法

  当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方法的作用便是如此。

(3). calculateDistanceToFinalSnap方法

  找到需要滑到的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可以实现页面切换的效果。我来简单的总结一下:

  1. 首先阻止RecyclerView的Fling事件,阻止的方式就是重写findTargetSnapPosition方法,当RecyclerView触发了Fling事件之后,直接滑动到下一个或者上一个。
  2. 如果RecyclerView没有触发Fling事件,或者Fling阶段未能滑动到正确位置,此时需要findSnapView方法和calculateDistanceToFinalSnap来保证滑动到正确的页面。

5. ScrollEventAdapter

  分析完PagerSnaHelper之后,我们来看看ScrollEventAdapter。前面我们已经说过了,ScrollEventAdapter的作用将RecyclerView的滑动事件转为ViewPager2的页面滑动事件。
  在分析源码之前,我们先来看看几个状态:

名称 含义
STATE_IDLE 表示当前ViewPager2处于停止状态
STATE_IN_PROGRESS_MANUAL_DRAG 表示当前ViewPager2处于手指拖动状态
STATE_IN_PROGRESS_SMOOTH_SCROLL 表示当前ViewPager2处于缓慢滑动的状态。这个状态只在调用了ViewPager2setCurrentItem方法才有可能出现。
STATE_IN_PROGRESS_IMMEDIATE_SCROLL 表示当前ViewPager2处于迅速滑动的状态。这个状态只在调用了ViewPager2setCurrentItem方法才有可能出现。
STATE_IN_PROGRESS_FAKE_DRAG 表示当前ViewPager2未使用手指滑动,而是通过FakerDrag实现的。

  ScrollEventAdapter实现的是OnScrollListener接口,所以,我们的重点放在两个实现方法里面。不过在正式这俩方法之前,我们先来了解几个方法,方便后面的理解。

方法名 含义
dispatchStateChanged 将状态改变的信息分发到OnPageChangeCallback监听器,不过需要注意的是:ViewPager2处于停止状态,同时调用了setCurrentItem方法来立即切换到某一个页面(注意,不是缓慢的切换),不会回调OnPageChangeCallback的方法。
dispatchSelected 分发选中页面的信息。
dispatchScrolled 分发页面滑动的相关信息。

  接下来,我们将正式分析onScrollStateChangedonScrolled

(1). onScrollStateChanged方法

  当RecyclerView的滑动状态发生变化,这个方法就会被调用。这个方法主要分为3个阶段,分别如下:

  1. 开始拖动,会调用startDrag方法表示拖动开始。
  2. 拖动手势的释放,此时ViewPager2会准备滑动到正确的位置。
  3. 滑动结束,此时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步我们需要注意如下两点:

  1. dispatchStateChanged方法的调用时机:1. 根本没有滑动,也就是说,onScrolled方法没有被调用;2. 滑动过,并且在上一次滑动中最后一次调用onScrolled方法的时候会被调用。
  2. dispatchSelected方法的调用时机:当mOffsetPx为0时会被调用,mOffsetPx为0表示当前ViewPager2根本未滑动。

(2). onScrolled方法

  在分析这个方法之前,我们看一下这个方法的代码:

    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方法里面主要做了两件事:

  1. 调用updateScrollEventValues方法更新ScrollEventValues里面的值。
  2. 调用相关方法,更新状态。

  关于更新ScrollEventValues里面的值,具体的细节是非常的简单,这里就不解释了。我简单的解释一下几个属性的含义:

名称 含义
mPosition 从开始滑动到滑动结束,一直记录着当前滑动到的位置。
mOffset 从一个页面滑动到另一个页面,记录着滑动的百分比。
mOffsetPx 记录着从开始滑动的页面与当前状态的滑动。每次滑动结束之后,会被重置。

  其实总的来说,ScrollEventAdapter的源码是非常简单,这里稍微复杂的就是各种状态的更新和相关的方法的回调。我来简单的总结一下:

  1. 当调用ViewPager2setAdapter方法时,此时应该回调一次dispatchSelected方法。
  2. 当调用setCurrentItem(x, false)方法,不会调用onScrollStateChanged方法,因而不会产生idle状态,因此,我们需要在onScrolled方法特殊处理(onScrolled方法会被调用)。
  3. 正常的拖动和释放,就是onScrollStateChanged方法和onScrolled方法的正常回调。

6. PageTransformerAdapter

  PageTransformerAdapter的作用将OnPageChangeCallback的事件转换成为一种特殊的事件,什么特殊的事件呢?我以一个例子来解释一下:

  1. 假设ViewPager2此时从A页面滑动到B页面,并且是从右往左滑动,其中A页面的变化范围:[0,-1);B页面的变化范围:[1,0)。
  2. 假设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);
        }
    }

  相信不用我解释上面的代码吧,大家应该都能看懂是怎么实现的。

7. FragmentStateAdapter

  接下来,我们将分析FragmentStateAdapter,看看它是加载Fragment的。在正式分析源码之前,我们先来几个成员变量。

变量名称 变量类型 含义
mFragments LongSparseArray<Fragment> key为itemId,value为Fragment。表示position与所放Fragment的对应关系(itemId与position有对应关系)
mSavedStates LongSparseArray<Fragment.SavedState> key为itemId,value为Fragment的状态
mItemIdToViewHolder LongSparseArray<Integer> key为itemId, value为ItemView的id。

  接下来,我们将分析在Adapter中比较重要的几个方法:

  1. onCreateViewHolder
  2. onBindViewHolder
  3. onViewAttachedToWindow
  4. onViewRecycled
  5. onFailedToRecycleView

  如上5个方法都与Fragment加载息息相关,我们一个一个的来看。

(1). onCreateViewHolder方法

  onCreateViewHolder方法主要创建ViewHolder,我们来简单看看怎么创建ViewHolder

    @NonNull
    @Override
    public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return FragmentViewHolder.create(parent);
    }

  其实就是调用了FragmentViewHolder的一个静态方法,具体细节这里就不展示了。

(2). onBindViewHolder方法

  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方法主要分为三步:

  1. 如果当前ItemView上已经加载了Fragment,并且不是同一个Fragment(ItemView被复用了),那么先移除掉ItemView上的Fragment。
  2. 初始化相关信息。
  3. 如果存在特殊情况,会走特殊情况。正常来说,都会经过onAttachToWindow方法来对Fragment进行加载。

   这其中,第三步是尤为重要的,不过这里,我们先分析它,待会详细的解释。

(3). onViewAttachedToWindow方法

  正常来说,ItemView都会在这个方法里面对Fragment进行加载,我们来看看代码:

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

  同样的,调用了placeFragmentInViewHolder方法加载Fragment。

(4). onViewRecycled方法

  当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方法操作差不多,这里就不过多分析了。

(5). placeFragmentInViewHolder方法

  接下来我们来分析placeFragmentInViewHolder方法,看看怎么加载Fragment。整个PageTransformerAdapter的核心点就在这个方法里面。
  在加载Fragment之前,我们需要判断几个状态:

  1. Fragment是否添加到ItemView 中。
  2. Fragment的View是否已经创建。
  3. 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所有流程,还是挺简单的,就是情况太多了。由于代码中的注释已经详细解释了每一步的含义,所以这里就不再赘述了。

8. 总结

  其实ViewPager2本身的源码是非常简单的,它的核心点就在各个组件当中,所以本文就不对ViewPager2的内部源码进行分析。到此为止,我们对ViewPager2的源码分析完毕,在这里,我在做一个小小的总结。

  1. ViewPager2本身是一个ViewGroup,没有特殊作用,只是用来装一个RecyclerView
  2. PagerSnapHelper实现页面切换效果的原因是calculateDistanceToFinalSnap阻止RecyclerView的Fling事件,直接让它滑动相邻页面;findSnapView方法和findTargetSnapPosition用来辅助滑动到正确的位置。
  3. ScrollEventAdapter的作用将RecyclerView的滑动事件转换成为ViewPager2的页面滑动事件。
  4. PageTransformerAdapter的作用将普通的页面滑动事件转换为特殊事件。
  5. FragmentStateAdapter完美实现了使用Adapter加载Fragment。在FragmentStateAdapter中,完美地考虑到ViewHolder的复用,Fragment加载和卸载。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容