美文网首页
RecyclerView源码阅读1

RecyclerView源码阅读1

作者: ZenCabin | 来源:发表于2018-09-17 21:21 被阅读21次

    RecyclerView的测量、布局、绘制

    代码基于26.1.0

    onMeasure过程

    RecyclerView继承自ViewGroup,作为view的父容器,onMeasure方法尤为重要。

    @Override
        protected void onMeasure(int widthSpec, int heightSpec) {
            // mLayout是一个LayoutManager对象,比如常用的LinearLayoutManager
            if (mLayout == null) {
                // 如果没有设置LayoutManager,会执行default方法,按照布局中指定spec和传参设置measure参数,界面会显示空白
                defaultOnMeasure(widthSpec, heightSpec);
                return;
            }
            // 系统给出的LayoutManager通常的mAutoMeasure均为true
            if (mLayout.mAutoMeasure) {
                final int widthMode = MeasureSpec.getMode(widthSpec);
                final int heightMode = MeasureSpec.getMode(heightSpec);
                final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                        && heightMode == MeasureSpec.EXACTLY;
                // 默认提供的LayoutManager均没有覆写这个方法,依然是default的measure
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                // 如果宽高的mode都是exactly,或者没有设置adapter的话,会跳过这里走layout过程
                if (skipMeasure || mAdapter == null) {
                    // 这里留个疑问,如果这里结束,就没有对子view进行测量的过程了,那在这种情况下,子view为什么仍然可以绘制出来?(在onLayout里会执行子view的测量)
                    return;
                }
                // 状态判断,step start为mLayoutStep的默认值,即初始化状态。
                if (mState.mLayoutStep == State.STEP_START) {
                    /**
                     * 第一步layout操作
                     * - 处理adapter的更新
                     * - 决定那个animation应该执行
                     * - 保存当前views的信息
                     * - 如果必要,执行pre-layout,并保存其信息
                     */
                    dispatchLayoutStep1();
                    // 执行完后,mLayoutStep变为State.STEP_LAYOUT
                }
                // 在第二步layout中设置规格尺寸,pre-layout应该使用旧规格执行以保持一致性
                mLayout.setMeasureSpecs(widthSpec, heightSpec);
                mState.mIsMeasuring = true;
                // 第二次layout,真正的layout过程
                dispatchLayoutStep2();
    
                // 通过遍历子view来获取RecyclerView的规格大小
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    
                // if RecyclerView has non-exact width and height and if there is at least one child
                // which also has non-exact width & height, we have to re-measure.
                // 如果RecyclerView的width和height都是non-exact的并且有至少一个子view也有non-exact的width和height,那么我们必须进行re-measure。(参考LinearLayoutManager中覆写的判断)
                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 {
                if (mHasFixedSize) {
                    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                    return;
                }
                // 自定义 onMeasure
                if (mAdapterUpdateDuringMeasure) {
                    eatRequestLayout();
                    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;
                    resumeRequestLayout(false);
                } else if (mState.mRunPredictiveAnimations) {
                    // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
                    // this means there is already an onMeasure() call performed to handle the pending
                    // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
                    // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
                    // because getViewForPosition() will crash when LM uses a child to measure.
                    setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
                    return;
                }
    
                if (mAdapter != null) {
                    mState.mItemCount = mAdapter.getItemCount();
                } else {
                    mState.mItemCount = 0;
                }
                eatRequestLayout();
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                resumeRequestLayout(false);
                mState.mInPreLayout = false; // clear
            }
        }
    

    真正的layout过程:dispatchLayoutStep2方法

    /**
         * 第二次布局,这里我们真正地为子views的最终状态而布局,
         * 这一步如果有必要可能会执行多次。(多次measure)
         */
        private void dispatchLayoutStep2() {
            eatRequestLayout();
            onEnterLayoutOrScroll();
            mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
            mAdapterHelper.consumeUpdatesInOnePass();
            // getItemCount是我们覆写的方法,在这里使用了
            mState.mItemCount = mAdapter.getItemCount();
            mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    
            // Step 2: Run layout(在dispatchLayoutStep1中会进行Step 1)
            mState.mInPreLayout = false;
            // ⭐⭐⭐ 这里可以看到RecyclerView把子view的layout策略交给了LayoutManager来实现,并将mRecycler和state传递了进去,mRecycler是缓存管理类
            mLayout.onLayoutChildren(mRecycler, mState);
    
            mState.mStructureChanged = false;
            mPendingSavedState = null;
    
            // onLayoutChildren may have caused client code to disable item animations; re-check
            mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
            mState.mLayoutStep = State.STEP_ANIMATIONS;
            onExitLayoutOrScroll();
            resumeRequestLayout(false);
        }
    

    以LinearLayoutManager为例,其onLayoutChildren方法如下,不同的LayoutManager有着不同的布局策略。方法一开始已经将layout的算法进行了详细的描述。

        /**
         * {@inheritDoc}
         */
        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            // 布局算法:
            // 1) 检查子view和其他变量,找到一个锚点的坐标和位置position
            // 2) 两个方向填充,从底部卷起,像顶部填充
            // 3) 从顶部卷起,像底部填充
            // 4) 全部填充
            // create layout state
            if (DEBUG) {
                Log.d(TAG, "is pre layout:" + state.isPreLayout());
            }
            if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
                if (state.getItemCount() == 0) {
                    removeAndRecycleAllViews(recycler);
                    return;
                }
            }
            if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
                mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
            }
    
            ensureLayoutState();
            mLayoutState.mRecycle = false;
            // 解决绘制方向的问题,默认false代表由左到右正向绘制,reverse为由右到左反向绘制
            resolveShouldLayoutReverse();
    
            final View focused = getFocusedChild();
            // mValid默认false,在判断内部被赋值true
            if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
                    || mPendingSavedState != null) {
                mAnchorInfo.reset();
                // 锚点方向,默认false,除非通过setStackFromEnd设置了相应的值
                mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
                // 这里计算了锚点的position和坐标
                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);
            }
            if (DEBUG) {
                Log.d(TAG, "Anchor info:" + mAnchorInfo);
            }
    
            // LLM may decide to layout items for "extra" pixels to account for scrolling target,
            // caching or predictive animations.
            int extraForStart;
            int extraForEnd;
            final int extra = getExtraLayoutSpace(state);
            // If the previous scroll delta was less than zero, the extra space should be laid out
            // at the start. Otherwise, it should be at the end.
            if (mLayoutState.mLastScrollDelta >= 0) {
                extraForEnd = extra;
                extraForStart = 0;
            } else {
                extraForStart = extra;
                extraForEnd = 0;
            }
            extraForStart += mOrientationHelper.getStartAfterPadding();
            extraForEnd += mOrientationHelper.getEndPadding();
            if (state.isPreLayout() && mPendingScrollPosition != 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;
                    }
                }
            }
            int startOffset;
            int endOffset;
            final int firstLayoutDirection;
            if (mAnchorInfo.mLayoutFromEnd) {
                firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                        : LayoutState.ITEM_DIRECTION_HEAD;
            } else {
                firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                        : LayoutState.ITEM_DIRECTION_TAIL;
            }
    
            onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
            detachAndScrapAttachedViews(recycler);
            mLayoutState.mInfinite = resolveIsInfinite();
            mLayoutState.mIsPreLayout = state.isPreLayout();
            if (mAnchorInfo.mLayoutFromEnd) {
                // ... 由下向上布局
            } else {
                // 向底部填充
                updateLayoutStateToFillEnd(mAnchorInfo);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
                final int lastElement = mLayoutState.mCurrentPosition;
                if (mLayoutState.mAvailable > 0) {
                    extraForStart += mLayoutState.mAvailable;
                }
                // 向顶部填充
                updateLayoutStateToFillStart(mAnchorInfo);
                mLayoutState.mExtra = 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.mExtra = extraForEnd;
                    fill(recycler, mLayoutState, state, false);
                    endOffset = mLayoutState.mOffset;
                }
            }
    
            // changes may cause gaps on the UI, try to fix them.
            // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
            // changed
            if (getChildCount() > 0) {
                // because layout from end may be changed by scroll to position
                // we re-calculate it.
                // find which side we should check for gaps.
                if (mShouldReverseLayout ^ mStackFromEnd) {
                    int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                    fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                } else {
                    int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                    fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                }
            }
            layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
            if (!state.isPreLayout()) {
                mOrientationHelper.onLayoutComplete();
            } else {
                mAnchorInfo.reset();
            }
            mLastStackFromEnd = mStackFromEnd;
            if (DEBUG) {
                validateChildOrder();
            }
        }
    

    这里fill是个关键点,我们会通过锚点像指定方向填充我们的item布局,如下图:


    布局流程-填充方向

    xxx更详细的描述xxx

    fill()方法:

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
                RecyclerView.State state, boolean stopOnFocusable) {
            // max offset we should set is 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;
                }
                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");
                }
                // 关键方法,这里在循环里不断调用来布局
                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;
                    // 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;
                }
            }
            return start - layoutState.mAvailable;
        }
    

    onLayout布局过程

    onLayout方法里有个dispatchLayout,在之前的onMeasure中可能layout过了,也可能没有,在这里会进行判断分别处理

    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // 分发layout过程
            dispatchLayout();
            // 
            mFirstLayoutComplete = true;
        }
    
    void dispatchLayout() {
            // mAdapter 和 mLayout 均未设置,return
            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;
            }
            // 更新状态变量mIsMeasuring 
            mState.mIsMeasuring = false;
            if (mState.mLayoutStep == State.STEP_START) {
                // 说明没有执行过dispatchLayoutStep1方法,执行后状态变为State.STEP_LAYOUT
                dispatchLayoutStep1();
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                    || mLayout.getHeight() != getHeight()) {
                // 开始的两步layout都做了,但是由于size变化了,需要在调用一次dispatchLayoutStep2方法
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else {
                // always make sure we sync them (to ensure mode is exact)
                mLayout.setExactMeasureSpecsFrom(this);
            }
            dispatchLayoutStep3();
        }
    

    相关文章

      网友评论

          本文标题:RecyclerView源码阅读1

          本文链接:https://www.haomeiwen.com/subject/bcgdnftx.html