RecycleView的绘制流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mingyunxiaohai/article/details/89296136

RecycleView继承自ViewGroup,绘制流程肯定也是遵循View的,测量(onMeasure),布局(onLayout),绘制(onDdraw)三大流程。所以从这三个地方开始查看,本篇是27.1.1版本的源码

protected void onMeasure(int widthSpec, int heightSpec) {
        //mLayout是LayoutManager如果为null,就走默认测量然后返回
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        //是否自动测量,比如常用的LinearLayoutManager和GridLayoutManager中默认直接返回true
        if (mLayout.isAutoMeasureEnabled()) {
            //获取长宽的测量规格
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            //内部还是调用了mRecyclerView.defaultOnMeasure走默认测量
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            //判断宽高的测量模式是不是精确测量
            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            //如果测量模式是精确值比如match_partent,写死的值或者adapter是null,结束测量
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }
            如果测量步骤是开始
            if (mState.mLayoutStep == State.STEP_START) {
                //布局的第一步,更新适配器,决定运行哪个动画,保存有关当前视图的信息,如果有必要,运行预测布局并保存其信息。
                dispatchLayoutStep1();
            }
            // 在第二步设置尺寸,预布局和旧尺寸要一致
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();

            //现在可以从子元素中得到宽和高
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            // 如果RecyclerView 没有精确的高度和宽度,并且只有一个孩子
            // 我们需要重新测量
            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(
                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                // now we can get the width and height from the children.
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        } else {
            //如果子view的大小不影响recycleview的大小
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            // 自定义测量
            if (mAdapterUpdateDuringMeasure) {
                startInterceptRequestLayout();
                onEnterLayoutOrScroll();
                processAdapterUpdatesAndSetAnimationFlags();
                onExitLayoutOrScroll();

                if (mState.mRunPredictiveAnimations) {
                    mState.mInPreLayout = true;
                } else {
                    // consume remaining updates to provide a consistent state with the layout pass.
                    mAdapterHelper.consumeUpdatesInOnePass();
                    mState.mInPreLayout = false;
                }
                mAdapterUpdateDuringMeasure = false;
                stopInterceptRequestLayout(false);
            } else if (mState.mRunPredictiveAnimations) {
                setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
                return;
            }

            if (mAdapter != null) {
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            startInterceptRequestLayout();
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            stopInterceptRequestLayout(false);
            mState.mInPreLayout = false; // clear
        }
    }

从上面的代码来看,先判断LayoutManager是否为null,如果是结束测量,然后判断测量模式是不是精确模式,也就是布局文件中设置match_parent和写死固定值,如果是结束测量。如果是wrap_content继续执行下面的方法。

如果是刚开始测量的状态,执行 dispatchLayoutStep1()方法,如果判断不是精准模式,在执行dispatchLayoutStep2()方法。dispatchLayoutStep1()主要是做一些清空和初始化操作,dispatchLayoutStep2()是真正的测量子view的宽高来决定recycleview的宽高。

初始化操作的代码就不看了,从dispatchLayoutStep1()的注释来看主要做了以下步骤:1. adapter的更新 2.决定应该运行哪个动画 3. 保存视图当前的信息 4. 如果需要,运行预测布局并保存信息。

下面来看dispatchLayoutStep2()方法

    private void dispatchLayoutStep2() {
        //开始中断布局请求
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();
        //设置状态为 布局和动画状态
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // 开始布局
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // 是否禁用动画
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
    }

从上面代码可以看到,开始布局那mLayout是LayoutManager对象,调用了LayoutManager中的onLayoutChildren方法,所以从这里我们可以知道,最终的布局是交给LayoutManager来完成的,系统提供了三个LayoutManager,线性的,网格的和瀑布流的,我们也可以自己继承LayoutManager来实现我们自己的LayoutManager。

所有item的布局都是在onLayoutChildren中实现,下面看LinearLayoutManager中的实现

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) 通过检子view和其他变量找到一个锚点坐标和锚点位置
        // 2) 从底部开始填充
        // 3) 从顶部开始填充
        // 4) 处理2和3两种方式的滚动
    ......
    final View focused = getFocusedChild();
        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // 计算锚点的位置
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
            }else{......}
     ......
      //一般情况下会选取最上(反向布局则是最下)的子View作为锚点参考
      if (mAnchorInfo.mLayoutFromEnd) {
            // 更新锚点坐标
            updateLayoutStateToFillStart(mAnchorInfo);
            //设置开始位置
            mLayoutState.mExtra = extraForStart;
            //开始填充
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // 更新锚点坐标
            updateLayoutStateToFillEnd(mAnchorInfo);
            //设置结束位置
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
             //开始填充
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        }else{
            ......
        }
        
        ......
}

这段代码比较多,本篇省略缓存的部分,只看绘制,主要是通过子view和其他变量找到锚点信息,通过锚点信息判断出是从下往上填充还是从上往下填充,updateLayoutStateToFillStart和updateLayoutStateToFillEnd不断更新锚点的值,其实就是计算屏幕的上方或者下方是否还有剩余的空间,在调用fill方法填充的时候,如果空间不足就不会执行填充的方法。然后在fill(recycler, mLayoutState, state, false)方法中填充View。

mAnchorInfo是AnchorInfo类用来保存锚点的信息,它有三个主要变量

  1. int mPosition;//锚点参考view在整个布局中的位置,是第几个
  2. int mCoordinate; //锚点的起始坐标
  3. boolean mLayoutFromEnd; 是否从尾部开始布局默认是false
   /**
     * 填充由layoutState给定的布局,它独立于LinearLayoutManager之外,稍微改一下可以用于我们的自定义的LayoutManager
     * @param recycler        当前的回收对象
     * @param layoutState     记录如何填充空间
     * @param state           控制滚动的步骤
     * @param stopOnFocusable 如果为true,则在第一个可聚焦的新子元素中停止填充
     * @return 它添加的像素数。适用于滚动函数
     */
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // 我们应该设置最大偏移量是 mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            //回收掉已经滑出屏幕的View
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //循环填充:进入条件有足够的空白空间和有更多数据
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            //向屏幕上填充一个View
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            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 || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                //如果进行了填充,减去填充使用的空间
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // 保留一个单独的剩余空间,因为mAvailable对于回收非常重要
                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;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

fill()方法中回收移除不可见的View,在屏幕上堆叠出可见的Viw,堆叠的原理就是看看当前界面有没有剩余的空间,如果有就拿一个新的View填充上去,填充工作使用layoutChunk(recycler, state, layoutState, layoutChunkResult)方法。

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
          //找到将要布局的View,先从缓存中找找不到在创建
          View view = layoutState.next(recycler);        
        ......
         LayoutParams params = (LayoutParams) view.getLayoutParams();
         //如果ViewHolder的列表不为null
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                //添加view,最终调用ViewGroup的addView方法
                addView(view);
            } else {
                //添加view, 最终调用ViewGroup的addView方法
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                //将要消失的view
                addDisappearingView(view);
            } else {
                //将要消失的view
                addDisappearingView(view, 0);
            }
        }
        //测量子view
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
         int left, top, right, bottom;
        //横排和竖排不同模式下 子view的四个边的边距
        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;
            }
        }
        //布局这个子view
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        
        ......
    }

  • layoutChunk方法就是找到一个子view,寻找子view是先去缓存中寻找找不到在通过调用createViewHolder()创建一个新的,缓存的逻辑此篇不往下看,只看绘制流程
  • 找到view之后,通过addView方法,加入到ViewGroup中
  • 通过measureChildWithMargins方法测量一个子view,会把我们通过recycleview.addItemDecoration方法设置的分割线的大小也计算进去,之后计算子view的四个边的边距
  • 最后通过layoutDecoratedWithMargins方法布局一个子view。layoutDecoratedWithMargins中调用就是view的layout方法。

到此dispatchLayoutStep2()这个方法算是看完了,到此所有子view的测量(measure)和布局(layout),然后执行dispatchLayoutStep2()这个方法后面的方法 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec) 根据子view的大小来设置自身(RecycleView)的大小。

RecycleView的onMeasure方法看完了,下面来看一下它的onLayout方法。

  protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

里面调用了 dispatchLayout()方法

void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        //如果状态还是开始状态,那么从新走一遍dispatchLayoutStep1();和dispatchLayoutStep2();
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // 数据更改后重新执行dispatchLayoutStep2();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // 确保是精准模式
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

在onMeasure的源码中我们知道,如果RecycleView设置的是精准模式(比如match_partent,写死的值)就直接返回了,那么它的状态还是State.STEP_START,到了onLayout方法后还是会执行dispatchLayoutStep1()和dispatchLayoutStep2()方法。

也就是说如果RecycleView设置的wrap_content,那么就先去测量和布局子view,根据子view的宽高来确定自身的宽高,反之如果RecycleView设置的是精准模式,就在onLayou中去测量和布局子veiw。

这里有出来一个dispatchLayoutStep3(),第三步

private void dispatchLayoutStep3() {
        mState.assertLayoutStep(State.STEP_ANIMATIONS);
        startInterceptRequestLayout();//开始中断布局
        onEnterLayoutOrScroll();
        mState.mLayoutStep = State.STEP_START;
        if (mState.mRunSimpleAnimations) {
            // 找到当前的位置,并处理更改动画
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                if (holder.shouldIgnore()) {
                    continue;
                }
                long key = getChangedHolderKey(holder);
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPostLayoutInformation(mState, holder);
                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
                if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                   //运行一个变更动画
                    final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                            oldChangeViewHolder);
                    final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                    if (oldDisappearing && oldChangeViewHolder == holder) {
                        // run disappear animation instead of change
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    } else {
                        final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                                oldChangeViewHolder);
                        // we add and remove so that any post info is merged.
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                        ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                        if (preInfo == null) {
                            handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                        } else {
                            animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                    oldDisappearing, newDisappearing);
                        }
                    }
                } else {
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                }
            }

            // 触发动画
            mViewInfoStore.process(mViewInfoProcessCallback);
        }

        mLayout.removeAndRecycleScrapInt(mRecycler);
        mState.mPreviousLayoutItemCount = mState.mItemCount;
        mDataSetHasChangedAfterLayout = false;
        mDispatchItemsChangedEvent = false;
        mState.mRunSimpleAnimations = false;

        mState.mRunPredictiveAnimations = false;
        mLayout.mRequestedSimpleAnimations = false;
        if (mRecycler.mChangedScrap != null) {
            mRecycler.mChangedScrap.clear();
        }
        if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
            // Initial prefetch has expanded cache, so reset until next prefetch.
            // This prevents initial prefetches from expanding the cache permanently.
            mLayout.mPrefetchMaxCountObserved = 0;
            mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
            mRecycler.updateViewCacheSize();
        }
        //布局完成
        mLayout.onLayoutCompleted(mState);
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
        mViewInfoStore.clear();
        if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
            dispatchOnScrolled(0, 0);
        }
        recoverFocusFromState();
        resetFocusInfo();
    }

可以看到,dispatchLayoutStep3()主要做了一些收尾的工作,这是布局的最后一步,保存视图和动画的信息,并做一些清理的工作。

onLayout方法就完了,最后看onDraw()方法

    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方法很简单,就是绘制分割线,我们通过recycleview.addItemDecoration方法设置的分割线就在这里开始绘制,调用的是我们自定义分割线的时候里面写的onDraw方法。绘制的区域就是我们在自定义分割线的时候重写的getItemOffsets方法中的设置的偏移。这部分的测量工作在dispatchLayoutStep2()->onLayoutChildren->fill->layoutChunk->measureChildWithMargins这个方法中。

     public void measureChildWithMargins(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;
            //计算长和宽的测量模式  加上margin,padding 和 分隔线的长宽
            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight()
                            + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom()
                            + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }

上面代码中就是获取线条的长宽,然后子veiw的可使用的宽高要减去这部分的值。

OK到这里RecycleView的绘制流程查看完成。

参考文章

https://www.jianshu.com/p/f91b41c8f487

https://www.jianshu.com/p/0c41bf63072a

https://www.jianshu.com/p/616ca453aa17

https://www.jianshu.com/p/8fa71076179d

猜你喜欢

转载自blog.csdn.net/mingyunxiaohai/article/details/89296136