RecyclerView核心原理源码分析

  • 概述

    RecyclerView是大家工作中经常用到的一个组件,它可以设置列表、网格列表、瀑布流网格列表,这三种布局模式都可以设置是横向的还是竖直方向的,而且可以设置是按照从左到右、从上到下的习惯顺序展示呢还是倒序展示。作为ListView和GridView的替代品,我们从源码的角度来分析一下它的原理,从而真正理解它的优势在哪里。

  • 设置布局模式

    RecyclerView里面有一个mLayout属性,他的类型是RecyclerView.LayoutManager,他负责保存RecyclerView的布局模式,默认是没有赋值的,所以一定需要手动设置,否则哪怕设置了数据也不会显示出任何东西,因为在onMeasure的时候一开始就会判断mLayout是不是null:

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
          ...
          ...
    }
    

    如果mLayout为null则会调用defaultOnMeasure方法,这个方法最终只会取minWidth/minHeight和padding的最小值。

    这个方法的调用顺序可以不用限制,只要在界面显示出来之前调用都可以,这是因为在setLayoutManager方法中会调用requestLayout方法重新布局。

    RecyclerView提供了三种布局模式:LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager,这三个都是继承自RecyclerView.LayoutManager,我们这里大部分流程按照LinearLayoutManager来看,只对另外两种不同的地方加以整理。

  • 根据数据显示UI的入口:requestLayout方法

    和ListView一样,RecyclerView根据数据填充界面同样是沿着requestLayout逻辑步骤完成的,知道这一点是至关重要的。调用requestLayout的地方有很多,比如setAdapter的时候、setLayoutManager的时候、notifyDataSetChanged的时候等等。

    我们知道,调用requestLayout之后会调用onMeasure方法:

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.isAutoMeasureEnabled()) {
           //使用RecyclerView的测量逻辑:dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
       //在这里这个方法没啥用,因为下面会调用RecyclerView的dispatchLayoutStep系列方法重新测量,三大LM都没有实现,因此相当于调用了defaultOnMeasure方法           
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    
            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
              //如果宽高是固定尺寸则不需要根据内容测量
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }
    
            if (mState.mLayoutStep == State.STEP_START) {
               //决策动画方式、保存当前视图信息等
                dispatchLayoutStep1();
            }
            
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
           // 真正的测量
            dispatchLayoutStep2();
    
            // 根据子View的宽高计算总大小
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    
            // 如果RecyclerView的宽高不确定且有子View的宽高也没确定则需要再次dispatchLayoutStep2测量
            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(
                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        } else {
            ...//通过对应LayoutManager的onMeasure方法实现进行测量
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            ...
        }
    }
    

    首先,mLayout.isAutoMeasureEnabled()来判断是否采用RecyclerView自己来测量,默认是false,即通过对应的LayoutManager来测量。

    mLayout.onMeasure方法是由LayoutManager中实现的:

    public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
            int heightSpec) {
        mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
    }
    

    也调用了defaultOnMeasure,所以其实就是上面取最小大小。

    LinearLayoutManager实现了isAutoMeasureEnabled并返回true,表示交给RecyclerView自行处理测量。

  • 自动测量

    自动测量分支逻辑中,如果RecyclerView的宽高测量模式不都是EXACTLY的话,说明根据内容来定,这时候会调用一次dispatchLayoutStep2方法先添加View,然后根据子View的总大小作为测量大小,然后这个时候如果判断子View中是否有wrap_content或match_parent的,如果有的话则进行第二次测量,可以看到,以宽度为例,这次测量时mWidth是通过getMeasuredWidth()获取的,即已经变成上次测量后的宽度作为第二次测量的限制宽度了,在layoutChunk方法(layoutChunk方法是添加View时会调用的,后面会说到)中会用到这个限制宽度:

    right = getWidth() - getPaddingRight();
    

    第一次的时候使用的是RecyclerView本身在父容器中的限制大小(即RecyclerView的LayoutParams指定的),而第二次使用的是内容布局之后的测量大小,所以这里二测测量的目的就是尝试进一步缩小测量范围(如果有需要的话)。

  • dispatchLayoutStep2方法

    上面提到diapaychLayoutStep2是做添加View的工作的,RecyclerView的核心就是这个方法的逻辑。

    diapaychLayoutStep2中会调用onLayoutChildren方法,这个方法就是真正的布局View的核心:mLayout.onLayoutChildren(mRecycler, mState)。LayoutManager中的这个方法如下:

    public void onLayoutChildren(Recycler recycler, State state) {
        Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
    }
    

    可以看到,需要子类重写,所以我们去LinearLayoutManager中去看,代码较长,我们只看主要逻辑即可。

    在这个方法中,通过mAnchorInfo.mLayoutFromEnd来判断是从上而下填充还是从下往上填充,这个值来自:

    private void resolveShouldLayoutReverse() {
        // A == B is the same result, but we rather keep it readable
        if (mOrientation == VERTICAL || !isLayoutRTL()) {
            mShouldReverseLayout = mReverseLayout;
        } else {
            mShouldReverseLayout = !mReverseLayout;
        }
    }
    

    mReverseLayout来自:

    public void setReverseLayout(boolean reverseLayout) {
        assertNotInLayoutOrScroll(null);
        if (reverseLayout == mReverseLayout) {
            return;
        }
        mReverseLayout = reverseLayout;
        requestLayout();
    }
    

    这个方法在构造时调用,结合上面的resolveShouldLayoutReverse方法可以知道,mShouldReverseLayout是由传入的和系统方向同时决定(规则就是负负得正)。

    然后会赋值mAchorInfo.mLayoutFromEnd:mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd。

    mStackFromEnd是和AbsListView中一样的作用,代表的是整个列表的方向,默认是false,异或操作也表示负负得正,即如果构造时传入了true并且调用了setStackFromEnd(true)方法,则列表会按照正序排列,这里可能是为了兼容老的编码习惯所以保留了mStackFromEnd的逻辑吧。

    mOrientationHelper在设置方向的时候生成的:

    public void setOrientation(@RecyclerView.Orientation int orientation) {
        if (orientation != HORIZONTAL && orientation != VERTICAL) {
            throw new IllegalArgumentException("invalid orientation:" + orientation);
        }
    
        assertNotInLayoutOrScroll(null);
    
        if (orientation != mOrientation || mOrientationHelper == null) {
            mOrientationHelper =
                    OrientationHelper.createOrientationHelper(this, orientation);
            mAnchorInfo.mOrientationHelper = mOrientationHelper;
            mOrientation = orientation;
            requestLayout();
        }
    }
    
    public static OrientationHelper createOrientationHelper(
            RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) {
        switch (orientation) {
            //这里会根据横向列表还是纵向列表生成不同的实现类
            case HORIZONTAL:
                return createHorizontalHelper(layoutManager);
            case VERTICAL:
                return createVerticalHelper(layoutManager);
        }
        throw new IllegalArgumentException("invalid orientation");
    }
    

    以竖直列表为例:

    public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            public int getEndAfterPadding() {
                return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
            }
    
            @Override
            public int getEnd() {
                return mLayoutManager.getHeight();
            }
    
            @Override
            public void offsetChildren(int amount) {
                mLayoutManager.offsetChildrenVertical(amount);
            }
    
            @Override
            public int getStartAfterPadding() {
                return mLayoutManager.getPaddingTop();
            }
              ...
            ...
        }
    

    可以看到,这个匿名类对象主要是用来封装调用LayoutManager的一些关于坐标测量方面的工作。

    每次重新layout,mAnchorInfo都会记录即将刷新成的列表的第一个可见View项的位置信息。接下来我们来看mAnchorInfo:

    static class AnchorInfo {
        OrientationHelper mOrientationHelper;
        int mPosition;
        int mCoordinate;
        boolean mLayoutFromEnd;
        boolean mValid;
    }
    

    通过代码发现,mPosition记录的是列表中显示的第一个(这里的第一个是相对于layout方向来说的,不是特指从上数第一个)View位于集合中的index下标;mCoordinate记录的是第一个可见View在RecyclerView中的起始位置(如果是竖向正序列表的话就是指top),这个可以从以下代码得出:

    anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
            - mPendingSavedState.mAnchorOffset;
    

    getEndAfterPadding()得到的是RecyclerView的不含padding(不含padding是因为padding区域不能画子View)的最末端(比如竖向正序列表的bottom),mPendingSavedState.mAnchorOffset是在onSaveInstanceState的时候赋值的:

    if (didLayoutFromEnd) {
        final View refChild = getChildClosestToEnd();
        state.mAnchorOffset = mOrientationHelper.getEndAfterPadding()
                - mOrientationHelper.getDecoratedEnd(refChild);
        state.mAnchorPosition = getPosition(refChild);
    } else {
        final View refChild = getChildClosestToStart();
        state.mAnchorPosition = getPosition(refChild);
        state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild)
                - mOrientationHelper.getStartAfterPadding();
    }
    

    getChildClosestToStart和getChildClosestToEnd方法都是取得当前布局顺序中第一个可见View,这里给它赋的值就是第一个可见View未显示出来部分的长度,例如mOrientationHelper.getDecoratedStart(以mOrientationHelper.为createVerticalHelper方法返回的为例):

    @Override
    public int getDecoratedStart(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return mLayoutManager.getDecoratedTop(view) - params.topMargin;
    }
    
    public int getDecoratedTop(@NonNull View child) {
        return child.getTop() - getTopDecorationHeight(child);
    }
    
    public int getTopDecorationHeight(@NonNull View child) {
        return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top;
    }
    

    child.getTop()返回的是当前子View位于父容器中的top,getTopDecorationHeight得到的是这个子View真正的顶端(可能这个顶端此时溢出在父容器之外,即不可见),这两个值相减得到的就是一个偏移量。

    综上所述,mCoordinate最终保存的就是这个第一个可见View相对于父容器的初始位置,如果有溢出的话则这个值是负的(正序来说),这里也可以大概能猜出先滚动到指定position再处理偏移的用意。

    知道了以上信息,下面的逻辑就好理解了。

    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // calculate anchor position and coordinate
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                    >= mOrientationHelper.getEndAfterPadding()
            || mOrientationHelper.getDecoratedEnd(focused)
            <= mOrientationHelper.getStartAfterPadding())) {
        // This case relates to when the anchor child is the focused view and due to layout
        // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
        // up after tapping an EditText which shrinks RV causing the focused view (The tapped
        // EditText which is the anchor child) to get kicked out of the screen. Will update the
        // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
        // the available space in layoutState will be calculated as negative preventing the
        // focused view from being laid out in fill.
        // Note that we won't update the anchor position between layout passes (refer to
        // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
        // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
        // child which can change between layout passes).
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
    }
    

    第一个分支表示三种情况,一个是初始化的时候,另一个是之前有过滚动的情况,还有就是onRestoreInstanceState恢复的时候(因为mPendingSavedState是这个方法调用时拿到的),这里都会重新设置mAnchorInfo,updateAnchorInfoForLayout如下:

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from pending information");
            }
            return;
        }
    
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from existing children");
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "deciding anchor info for fresh state");
        }
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }
    

    首先执行的是updateAnchorFromPendingData方法,这个方法是获取到上次滚动的位置信息或者onRestoreInstanceState中恢复的位置,把信息赋值到mAnchor中,里面用到的mPendingScrollPosition表示的调用scrollToPosition时传入的position。

    然后如果之前没有需要恢复的位置,则执行updateAnchorFromChildren方法,这个方法首先会尝试找出拥有焦点的子View并且必须是可见的然后定位到它的位置,若没有则定位到集合中的第一个View,当然这里说的定位指的是锁定第一个要显示的View的位置信息。

    最后assignCoordinateFromPadding方法会采用RecyclerView的初始位置作为mCoordinate,表示若是没有有效位置就使用初始位置显示。

    在了解了AnchorInfo之后,updateAnchorInfoForLayout方法里面调用的这几个方法的逻辑很好理解,就不贴代码了。

    回到onLayoutChildren方法,这里的第二个分支就是处理含有焦点的子View的,逻辑在assignFromViewAndKeepVisibleRect中,updateAnchorFromChildren方法中符合情况时也调用了这个方法来处理焦点View。

    这个方法中一开始:

    final int spaceChange = mOrientationHelper.getTotalSpaceChange();
    if (spaceChange >= 0) {
        assignFromView(child, position);
        return;
    }
    

    这是为了检查是否RecyclerView的内容区域layout方向上的长度发生了变化,如果变化了则使用初始位置。

    然后我们取正序的逻辑分支代码来看一下:

    final int childStart = mOrientationHelper.getDecoratedStart(child);
    final int startMargin = childStart - mOrientationHelper.getStartAfterPadding();
    mCoordinate = childStart;
    if (startMargin > 0) { // we have room to fix end as well
        final int estimatedEnd = childStart
                + mOrientationHelper.getDecoratedMeasurement(child);
        final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding()
                - spaceChange;
        final int previousEndMargin = previousLayoutEnd
                - mOrientationHelper.getDecoratedEnd(child);
        final int endReference = mOrientationHelper.getEndAfterPadding()
                - Math.min(0, previousEndMargin);
        final int endMargin = endReference - estimatedEnd;
        if (endMargin < 0) {
            mCoordinate -= Math.min(startMargin, -endMargin);
        }
    }
    

    可以看到,mOrientationHelper.getDecoratedStart(child)直接赋给了mCoordinate,为什么这里没有用child.top减去这个值呢,因为焦点View恢复的时候是需要完全展示出来的。最后if代码块中的逻辑主要是处理因为spaceChange变化的偏移(可绘制长度变化了,焦点View的位置也要跟着同步移动)。

    回到onLayoutChildren方法,接下来执行代码:

    mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
            ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    

    mLayoutState.mLastScrollDelta记录的是当前布局方向上的位移量,正负代表方向,如果是正数则表示是向下滚动,则这里记录layoutDirection是向下填充。

    mReusableIntPair(长度为2的int数组)用来保存上下可滚动的长度,根据mLayoutState.mLayoutDirection来决定,通过calculateExtraLayoutSpace方法设置:

    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
            @NonNull int[] extraLayoutSpace) {
        int extraLayoutSpaceStart = 0;
        int extraLayoutSpaceEnd = 0;
    
        // If calculateExtraLayoutSpace is not overridden, call the
        // deprecated getExtraLayoutSpace for backwards compatibility
        @SuppressWarnings("deprecation")
        int extraScrollSpace = getExtraLayoutSpace(state);
        if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            extraLayoutSpaceStart = extraScrollSpace;
        } else {
            extraLayoutSpaceEnd = extraScrollSpace;
        }
    
        extraLayoutSpace[0] = extraLayoutSpaceStart;
        extraLayoutSpace[1] = extraLayoutSpaceEnd;
    }
    

    这里的state是从onMeasure传过来的,记录的是RecyclerView有关的当前信息。

    if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
            && mPendingScrollPositionOffset != INVALID_OFFSET) {
        // if the child is visible and we are going to move it around, we should layout
        // extra items in the opposite direction to make sure new items animate nicely
        // instead of just fading in
        final View existing = findViewByPosition(mPendingScrollPosition);
        if (existing != null) {
            final int current;
            final int upcomingOffset;
            if (mShouldReverseLayout) {
                current = mOrientationHelper.getEndAfterPadding()
                        - mOrientationHelper.getDecoratedEnd(existing);
                upcomingOffset = current - mPendingScrollPositionOffset;
            } else {
                current = mOrientationHelper.getDecoratedStart(existing)
                        - mOrientationHelper.getStartAfterPadding();
                upcomingOffset = mPendingScrollPositionOffset - current;
            }
            if (upcomingOffset > 0) {
                extraForStart += upcomingOffset;
            } else {
                extraForEnd -= upcomingOffset;
            }
        }
    }
    

    这里是计算屏幕之外的layout长度。

    接下来执行的detachAndScrapAttachedViews(recycler)方法会把之前的View回收以作复用,在这个方法里会循环调用getChildAt获取View然后调用scrapOrRecycleView方法:

    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.shouldIgnore()) {
            if (DEBUG) {
                Log.d(TAG, "ignoring view " + viewHolder);
            }
            return;
        }
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            removeViewAt(index);
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            detachViewAt(index);
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }
    

    这里会通过View的RecyclerView.LayoutParam拿到它的ViewHolder,回收复用都是针对ViewHolder。这里有两种情况,一种是假定短时间内完全不再需要的Item(大胆猜测这是View滚动到离当前屏幕显示positon范围比较远的时候),另一种是暂时不可见但是假定短时间内可能需要显示的Item,比如刚刚滑出屏幕不远的。第一种是直接调用removeViewAt移除View,然后调用recycleViewHolderInternal方法,这个方法里:

    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // 若超出最大长度则把第一个加入的(也就是最久没被使用的)删去
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
    
            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // 跳过最近预先载入的部分
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } 
    

    可见会把remove的View的ViewHolder放到mCachedViews中,如果是不合法的项则调用addViewHolderToRecycledViewPool方法,这个方法中又调用getRecycledViewPool().putRecycledView(holder):

    public void putRecycledView(ViewHolder scrap) {
        final int viewType = scrap.getItemViewType();
        final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
        if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
            return;
        }
        if (DEBUG && scrapHeap.contains(scrap)) {
            throw new IllegalArgumentException("this scrap item already exists");
        }
        scrap.resetInternal();
        scrapHeap.add(scrap);
    }
    

    mScrap中按照viewType保存了一系列的ScrapData,每个ScrapData都有一个mScrapHeap,保存了所以属于该viewtype的ViewHolder,默认这个集合不能超过五个,即每种类型最多保存前五个ViewHolder且不会被新的替换。总结,这里会分两个缓存关卡,首先尝试放在mCachedViews中,否则尝试放在mScrapHeap中。recycleViewHolderInternal方法最后还会调用mViewInfoStore.removeViewHolder(holder)移除mViewInfoStore中的ViewHolder。

    第二种是不会移除View的,也就是说在屏幕可见范围之外还是保存了一定数量的View实例(ViewHolder)的。recycler.scrapView(view)如下:

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                throw new IllegalArgumentException("Called scrap view with an invalid view."
                        + " Invalid views cannot be reused from scrap, they should rebound from"
                        + " recycler pool." + exceptionLabel());
            }
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);
        } else {
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }
    

    这里是分别处理更新的和不需要更新的ViewHolder,前者会放在mChangedScrap中,后者会放在mAttchedScrap。中,mRecyclerView.mViewInfoStore.onViewDetached(viewHolder)会调用removeFromDisappearedInLayout(viewHolder):

    void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            return;
        }
        record.flags &= ~FLAG_DISAPPEARED;
    }
    

    mLayoutHolderMap猜测是来记录界面上需要显示的项,渲染的时候应该会用到,这里会修改其中每个InfoRecord的flags。

    回到onLayoutChildren中,接下来开始填充:

    // fill towards end
    updateLayoutStateToFillEnd(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForEnd;
    fill(recycler, mLayoutState, state, false);
    endOffset = mLayoutState.mOffset;
    final int lastElement = mLayoutState.mCurrentPosition;
    if (mLayoutState.mAvailable > 0) {
        extraForStart += mLayoutState.mAvailable;
    }
    // fill towards start
    updateLayoutStateToFillStart(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForStart;
    mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
    fill(recycler, mLayoutState, state, false);
    startOffset = mLayoutState.mOffset;
    
    if (mLayoutState.mAvailable > 0) {
        extraForEnd = mLayoutState.mAvailable;
        // start could not consume all it should. add more items towards end
        updateLayoutStateToFillEnd(lastElement, endOffset);
        mLayoutState.mExtraFillSpace = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
    }
    

    我截取的是一段正序的逻辑代码,就按照我们习惯的从上到下来理解。updateLayoutStateToFillEnd和updateLayoutStateToFillStart是靠mAchorInfo给mLayoutState赋值,fill方法如下:

    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
          //计算新View添加后的位移量
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        /**
         * Consume the available space if:
         * * layoutChunk did not request to be ignored
         * * OR we are laying out scrap children
         * * OR we are not doing pre-layout
         */
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
              //layoutState.mAvailable也会递减
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
    
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        if (stopOnFocusable && layoutChunkResult.mFocusable) {
            break;
        }
    }
    

    layoutChunk用来添加View:

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
    
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }
    

    layoutState.next方法:

    View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextViewFromScrapList();
        }
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }
    

    先从mScrapList中取,否则走recycler.getViewForPosition,这个方法最终会调用tryGetViewHolderForPositionByDeadline方法,这个方法较长,逻辑就是依次尝试从上面我们看过的复用集合里取,调用顺序即为获取顺序:getChangedScrapViewForPosition、getScrapOrHiddenOrCachedHolderForPosition、getScrapOrCachedViewForId、getRecycledViewPool().getRecycledView、mAdapter.createViewHolder,代表从mChangedScrap、mAttachedScrap、mCachedViews、mRecyclerPool、mAdapter.onCreateViewHolder中获取。

    取到或者创建ViewHolder之后就是添加View:

    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    

    addDisappearingView最终会调用addViewInt方法,这个方法里会把之前remove或者detach的重新绑定到父容器上,这就是“添加”之前显示过的且还存在在复用集合中的View。

    然后调用measureChildWithMargins方法重新测量child的尺寸,接下来就是计算child的新的layout信息,然后调用layoutDecoratedWithMargins方法重新排列:

    public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
            int bottom) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Rect insets = lp.mDecorInsets;
        child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                right - insets.right - lp.rightMargin,
                bottom - insets.bottom - lp.bottomMargin);
    }
    
  • ItemDecoration

    RecyclerView有一个addItemDecoration方法可以添加分割线。

    它的使用在onDraw方法中得到体现:

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
    
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
    

    可见,每次onDraw首先会调用super.onDraw方法,说明分割线跟在每一项的后面。调用for循环执行添加的所有ItemDecoration的onDraw方法。

    我们以DividerItemDecoration实现类来看看onDraw做了什么:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
    

    这里的mDivider是一个Drawable,mOrientation和RecyclerView的方向是一致的,如果是纵向的就是drawVertival:

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }
    
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }
    

    只是这样我们会发现分割线会被画在了child上,我们还需要实现一个getItemOffsets方法,在这里就是:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
    

    这个方法在哪里调用的呢,在前面onMeasure流程中,调用measureChild方法时:

    public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
        widthUsed += insets.left + insets.right;
        heightUsed += insets.top + insets.bottom;
        final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                canScrollHorizontally());
        final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                canScrollVertically());
        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
    }
    

    getItemDecorInsetsForChild方法中会调用getItemOffsets:

    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }
    
        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }
    

    所以在测量子View的时候就已经把分割线的宽度加入到child的尺寸中去了,在调用getDecoratedBoundsWithMargins的时候拿到的就是预留出分割线宽度的Rect。因为可以添加多个ItemDecoration,所以这里可以在onDraw中有些自定义操作来实现一些想要的效果。

    关于dispatchLayoutStep1和dispatchLayoutStep3和RecyclerView的动画关系比较密切,这里先不说他。

  • 总结

    和ListView相比,RecyclerView有着一些更高级的处理。比如:

    1. 在架构上使用不同的LayoutManager使其拥有更多的布局模式,而且有必要的话你可以实现自己的LayoutManager;
    2. 我们不需要额外实现自己的ViewHolder,RecyclerView并不是在滚动的时候即时地去调用Adapter的方法创建ViewHolder,相比入保存View然后通过setTag保存ViewHolder,RecyclerView使用保存ViewHolder的方式来管理View及其相关信息,更加的全面和方便;
    3. ListView创建View和绑定数据在一个地方,即getView方法,所以getView方法在滚动显示不可见View的时候总是会调用这个方法,而RecyclerView并不是总会创建View,所以他有一个另外的绑定数据的方法;
    4. ListView在滚动的时候会频繁的addView和removeView,会很影响性能,而RecyclerView会保存一部分屏幕外不可见的View,这样在滚动的时候就会更快的显示,而且设置了最大数量避免引起OOM。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容