5

RecycleView的绘制流程

 3 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/89296136
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

RecycleView的绘制流程

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK