RecyclerView 静态布局实现过程解析:如何构建高性能的列表

news2024/11/24 3:17:39

作者:maxcion

Recyclerview在日常开发中所使用的控件中绝对是顶流一般的存在,想嚼它这个想法一次两次了。在网上也看了很多关于Recyclerview源码解析的文章,大佬们写的都很深刻,但是对于像我们这种储备知识不足的小白读者来说,那感觉就像外地人坐上了黑车,你说咋走就咋走。

测量流程

Recyclerview既然是一个View,那他的测量流程必然会走到onMeasure()

onMeasure

 @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        //如果没设置LayoutManager那就不用走测量和布局流程
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }

        //LinearLayoutManager isAutoMeasureEnabled() 默认是true
        if (mLayout.isAutoMeasureEnabled()) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);

            /**
             * This specific call should be considered deprecated and replaced with
             * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
             * break existing third party code but all documentation directs developers to not
             * override {@link LayoutManager#onMeasure(int, int)} when
             * {@link LayoutManager#isAutoMeasureEnabled()} returns true.
             */
            //其实这里从源码注释中可以看到这个地方已经废弃了,但是之所以没有删除是为了保证第三方库的稳定性
            //所以这里直接跳过
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            //在这里我们判断给的宽高约束是否都是EXACTLY,如果是的话那就不用走复杂的测量逻辑了
            mLastAutoMeasureSkippedDueToExact =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
                return;
            }

            //这里mState.mLayoutStep的默认值就是State.STEP_START
            //但是呢,在静态布局的时候我们不用管这个流程,因为这里主要是
            //为了处理动画相关的逻辑,这里会在第四章重点介绍的
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }

            //将目前测量出来的大小传递给LayoutManager,因为最终复测layout children
            //的LayoutManager,所以他需要只要大小
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            //这里要重点关注,因为这里是真正执行layout的地方
            dispatchLayoutStep2();

            //因为dispatchLayoutStep2()执行了layout逻辑,所以Recyclerview已经被
            //children撑起来了,那就可以通过children获取RV的尺寸
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            //如果layout还没测量出明确的尺寸,就需要第二次测量
            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);
            }

            mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth();
            mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight();
        } else {
           ...
        }
    }

为了对代码有全面的了解,我选择在代码中以注释的形式讲解,然后再对代码中的细节进行详细讲解,关于onMeasure的大体流程都在上方面以注释的形式进行解释了,从注释中我们可以看到我们需要重点关注的逻辑有dispatchLayoutStep1()dispatchLayoutStep2()

Recyclerview.dispatchLayoutStep1()


/**
 * The first step of a layout where we;
 * - process adapter updates
 * - decide which animation should run
 * - save information about current views
 * - If necessary, run predictive layout and save its information
 */

private void dispatchLayoutStep1() {
    ...
    ...
    mState.mLayoutStep = State.STEP_LAYOUT;
}

从官方注释中能看到dispatchLayoutStep1()主要有四个职责:

  • 处理adapter发起的布局更新
  • 决定执行什么样的动画
  • 保存当前children的特定信息
  • 如果有必要就执行predictive动画

从注释上也看看出来这段逻辑和静态布局几乎没有关系,但是在这里把代码贴出来而且仅贴了最后一行,因为这里更新了mState.mLayoutStep的状态,在dispatchLayoutStep2()中会用到

Recyclerview.dispatchLayoutStep2()

private void dispatchLayoutStep2() {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    //因为在 dispatchLayoutStep1()中最后一行代码设置为State.STEP_LAYOUT,所以正常往下走
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    //这里主要是处理adapter 的增删改的逻辑
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    if (mPendingSavedState != null && mAdapter.canRestoreState()) {
        if (mPendingSavedState.mLayoutState != null) {
            mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
        }
        mPendingSavedState = null;
    }
    // Step 2: Run layout
    //标记当前已经不是预布局(dispatchLayoutStep1)阶段
    mState.mInPreLayout = false;
    //交给LayoutManager进行布局
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;

    // onLayoutChildren may have caused client code to disable item animations; re-check
    //检查是否支持动画
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    //更新布局阶段的状态
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
}

因为这章只讲静态布局,所以真正相关的逻辑只在mLayout.onLayoutChildren(mRecycler, mState),逻辑已经进入LM(LayoutManager)中了,那这里我们就以LLM(LinearLayoutManager)为例

LLM.onLayoutChildren()

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // layout algorithm:
    // 1) by checking children and other variables, find an anchor coordinate and an anchor
    //  item position.
    // 2) fill towards start, stacking from bottom
    // 3) fill towards end, stacking from top
    // 4) scroll to fulfill requirements like stack from bottom.
    // create layout state
    if (DEBUG) {
        Log.d(TAG, "is pre layout:" + state.isPreLayout());
    }
    if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
        if (state.getItemCount() == 0) {
            removeAndRecycleAllViews(recycler);
            return;
        }
    }
    if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
        mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
    }

    //创建一个LayoutState对象,这是一个Layout流程标志位统一管理对象
    ensureLayoutState();
    mLayoutState.mRecycle = false;
    // resolve layout direction
    //判断当前设置的布局方向,与构造函数中的reverseLayout参数有关
    resolveShouldLayoutReverse();

    final View focused = getFocusedChild();

    //第一次进来时 mAnchorInfo.mValid 是false
    if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        //mStackFromEnd的取值是根据自定义属性stackFromEnd的传值定的,默认是false,这里
        //我们一reverseLayout为false情况为准,所以mAnchorInfo.mLayoutFromEnd最终为false,
        //下面会用到
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // calculate anchor position and coordinate
        //更新AnchorInfo,默认情况下,我们肯定都是认为布局children都是从
        //RV的顶部然后然后布局到RV的地步,我们静态布局确实是这样的
        //其实这里做得主要逻辑就是找到第一个没被remove的child
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
            >= mOrientationHelper.getEndAfterPadding()
            || mOrientationHelper.getDecoratedEnd(focused)
            <= mOrientationHelper.getStartAfterPadding())) {
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(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.

    // 如果 mLastScrollDelta 大于等于 0,则说明当前的布局方向是向末尾(即底部)方向布局(即 LayoutState.LAYOUT_END),
    // 否则就是向起始(即顶部)方向布局(即 LayoutState.LAYOUT_START)
    mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
            ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    //①计算布局开始和结束的位置
    //如果是从顶部往底部布局mReusableIntPair[0] = 0,mReusableIntPair[1] = RV.height - padding
    //反之mReusableIntPair[0]与mReusableIntPair[1]值互换
    //后面同列出这个逻辑的详细代码
    calculateExtraLayoutSpace(state, mReusableIntPair);
    //在根据padding和margin计算最终的开始与结束位置
    int extraForStart = Math.max(0, mReusableIntPair[0])
            + mOrientationHelper.getStartAfterPadding();
    int extraForEnd = Math.max(0, mReusableIntPair[1])
            + mOrientationHelper.getEndPadding();
    //这里我们讨论的是静态布局,而且在dispatchLayoutStep2中已经把isPreLayout置为false了
    //所以这里的if不成立
    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;
            }
        }
    }
    int startOffset;
    int endOffset;
    final int firstLayoutDirection;
    //根据mLayoutFromEnd和mShouldReverseLayout判断第一个child布局方向
    //因为mLayoutFromEnd和mShouldReverseLayout都是false,
    //所以firstLayoutDirection ==LayoutState.ITEM_DIRECTION_TAIL
    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();
    // noRecycleSpace not needed: recycling doesn't happen in below's fill
    // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
    mLayoutState.mNoRecycleSpace = 0;
    //上面因为mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;逻辑
    //所以这里是false
    if (mAnchorInfo.mLayoutFromEnd) {
        // fill towards start
        //将mAnchorInfo 布局信息设置给mLayoutState
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForStart;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        final int firstElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForEnd += mLayoutState.mAvailable;
        }
        // fill towards end
        //再从anchorView反方向填充
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    } else {
        // fill towards end
        //② 根据AnchorInfo来更新LayoutState的内容,主要同步布局方向,
        //从RV的什么位置开始布局(比如从RV y轴100的地方开始向底部布局)
        //还剩多少可用空间可以用来布局
        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;
        }
    }

    ...

    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    if (!state.isPreLayout()) {
        mOrientationHelper.onLayoutComplete();
    } else {
        mAnchorInfo.reset();
    }
    mLastStackFromEnd = mStackFromEnd;
    if (DEBUG) {
        validateChildOrder();
    }
}

正如官方注释所说,onLayoutChildren()主要有以下职责:

  1. 参数检查并找到anchorView
  2. 向前填充child
  3. 向后填充child
  4. XXXXXXX,我没懂…

懵逼了有没有,只是一个lauout流程,怎么又是找AnchorView,又从向前填充,又是向后填充!!!,正常情况我们都是一把梭,从头填充到尾然后打完收工.

在我们这个章节里其实就是找AnchorView没什么逻辑,但是找到AnchorView这个机制也不是为了静态布局合计的,更多的是为了Adapter增删改设计的,但是这里还是想讲一下,所以我们先看一下是怎么找到AnchorView,然后再说一下为什么要有AnchorView这个设计.

LLM.updateAnchorInfoForLayout()

private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
        AnchorInfo anchorInfo) {
        //这里应该和smoothScrooll相关,暂时不用管
    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;
}

private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
        RecyclerView.State state, AnchorInfo anchorInfo) {
    if (getChildCount() == 0) {
        return false;
    }
    //这里和FocusedChild相关,也不用管
    final View focused = getFocusedChild();
    if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
        anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
        return true;
    }
    if (mLastStackFromEnd != mStackFromEnd) {
        return false;
    }
    //① 主要看这里他是如何找到anchorView的
    View referenceChild =
            findReferenceChild(
                    recycler,
                    state,
                    anchorInfo.mLayoutFromEnd,
                    mStackFromEnd);

if (referenceChild != null) {
    anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
    // If all visible views are removed in 1 pass, reference child might be out of bounds.
    // If that is the case, offset it back to 0 so that we use these pre-layout children.
    //如果不是preLayout且支持Predictive动画
    //就会执行以下逻辑
    if (!state.isPreLayout() && supportsPredictiveItemAnimations()) {
        // validate this child is at least partially visible. if not, offset it to start
        final int childStart = mOrientationHelper.getDecoratedStart(referenceChild);
        final int childEnd = mOrientationHelper.getDecoratedEnd(referenceChild);
        final int boundsStart = mOrientationHelper.getStartAfterPadding();
        final int boundsEnd = mOrientationHelper.getEndAfterPadding();
        // b/148869110: usually if childStart >= boundsEnd the child is out of
        // bounds, except if the child is 0 pixels!
        boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart;
        boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd;
        //判断当前找到的view是否在RV的屏幕外部
        if (outOfBoundsBefore || outOfBoundsAfter) {
            anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? boundsEnd : boundsStart;
        }
    }

        return true;
    }
    return false;
}

View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
        boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) {
    ensureLayoutState();

    // Determine which direction through the view children we are going iterate.
    int start = 0;
    int end = getChildCount();
    int diff = 1;
    if (traverseChildrenInReverseOrder) {
        start = getChildCount() - 1;
        end = -1;
        diff = -1;
    }

    int itemCount = state.getItemCount();

    final int boundsStart = mOrientationHelper.getStartAfterPadding();
    final int boundsEnd = mOrientationHelper.getEndAfterPadding();

    View invalidMatch = null;
    View bestFirstFind = null;
    View bestSecondFind = null;
    //上面的代码不是很重要,都是为了这里的for循环做铺垫的,主要就是
    //如果你是从顶部向底部布局,那么for循环就是从0 开始,for循环每次+diff(1)
    //如果你是从底部布局,那么for循环就是从childCount-1开始,每次+diff(-1)
    //重要逻辑在这个for循环中:这里面的逻辑就是找到第一个没有被remove的child
    //如果没有找到就在第二优先级中寻找,然后第三优先级
    for (int i = start; i != end; i += diff) {
        final View view = getChildAt(i);
        final int position = getPosition(view);
        final int childStart = mOrientationHelper.getDecoratedStart(view);
        final int childEnd = mOrientationHelper.getDecoratedEnd(view);
        if (position >= 0 && position < itemCount) {
            if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
                if (invalidMatch == null) {
                    invalidMatch = view; // removed item, least preferred
                }
            } else {
                // b/148869110: usually if childStart >= boundsEnd the child is out of
                // bounds, except if the child is 0 pixels!
                boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart;
                boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd;
                if (outOfBoundsBefore || outOfBoundsAfter) {
                    // The item is out of bounds.
                    // We want to find the items closest to the in bounds items and because we
                    // are always going through the items linearly, the 2 items we want are the
                    // last out of bounds item on the side we start searching on, and the first
                    // out of bounds item on the side we are ending on.  The side that we are
                    // ending on ultimately takes priority because we want items later in the
                    // layout to move forward if no in bounds anchors are found.
                    if (layoutFromEnd) {
                        if (outOfBoundsAfter) {
                            bestFirstFind = view;
                        } else if (bestSecondFind == null) {
                            bestSecondFind = view;
                        }
                    } else {
                        if (outOfBoundsBefore) {
                            bestFirstFind = view;
                        } else if (bestSecondFind == null) {
                            bestSecondFind = view;
                        }
                    }
                } else {
                    // We found an in bounds item, greedily return it.
                    return view;
                }
            }
        }
    }
    // We didn't find an in bounds item so we will settle for an item in this order:
    // 1\. bestSecondFind
    // 2\. bestFirstFind
    // 3\. invalidMatch
    return bestSecondFind != null ? bestSecondFind :
            (bestFirstFind != null ? bestFirstFind : invalidMatch);
}

简单概括就是下面这张图:

为什么要找到找AnchorView呢?我们假设一个场景:

在一个高度为100dp的RV中,他的每个item的高度是20dp,现在因为增/删操作我我们的anchorView的坐标在(0,40)

现在有了AnchorView的坐标了,那Recyclerview是怎么使用的呢?他将layout流程分为两步,第一步:从AnchorView向顶部开始布局,第二步:从AnchorView从底部开始布局,一上一下就把整个屏幕布局满了.

LLM.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
        extraLayoutSpaceStart = extraScrollSpace;
    } else {
        extraLayoutSpaceEnd = extraScrollSpace;
    }

    extraLayoutSpace[0] = extraLayoutSpaceStart;
    extraLayoutSpace[1] = extraLayoutSpaceEnd;
}

//获取RV总共可布局空间大小
protected int getExtraLayoutSpace(RecyclerView.State state) {
    if (state.hasTargetScrollPosition()) {
        //① 这里我们以LLM 的垂直布局为例
        return mOrientationHelper.getTotalSpace();
    } else {
        return 0;
    }
}

@Override
public int getTotalSpace() {
//通过RV的高度减去paddingVertical
    return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
            - mLayoutManager.getPaddingBottom();
}

从上面的代码可以看出calculateExtraLayoutSpace()他真的只是计算可布局的空间大小.其实最重要的就是layout流程,但是要layout就必须要找到AnchorView,现在我们有AnchorView我们就可以开始看layout流程了,所有layout流程都是从fill()开始的.

LLM.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);
    }
    //计算总共有多少空间可以用来摆放child
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    //一个用来保存每次布局一个child的结果类,比如一个child消费了多少空间
    //是否应该真实的计算这个child消费的空间(预布局的时候有些child虽然消费了空间,
    // 但是不应该不参与真正的空间剩余空间的计算)
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    //只要还有空间和item就进行布局layoutchunk
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //重置上一次布局child的结果
        layoutChunkResult.resetInternal();
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.beginSection("LLM LayoutChunk");
        }
        //①这里是真正layout child的逻辑
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.endSection();
        }
        if (layoutChunkResult.mFinished) {
            break;
        }
        //layoutState.mLayoutDirection的值是 1或者-1 所以这里是 乘法
        //如果是从顶部往底部填充,当前填充的是第三个child 且每个高度是10dp,那么layoutState.mOffset的值
        //就是上次填充时的偏移量 + 这次填充child的高度
        //如果是从底部往顶部填充,那就是次填充时的偏移量 - 这次填充child的高度
        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
         */
        //判断是否要真正的消费当前child参与布局所消费的高度
        //从判断条件中可以看到预布局和这个有关,不过预布局等后面几章会详细说的
        //这里就是同步目前还剩多少空间可以用来布局
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.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;
        }
    }
    if (DEBUG) {
        validateChildOrder();
    }
    return start - layoutState.mAvailable;
}

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    //这里创建一个childView(暂时理解过创建一个child,里面涉及从缓存中获取child)
    //但是我们这张不考虑缓存的逻辑,所以这里直接认定为创建了一个child,后面章节会
    //详细的介绍这里的
    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();
    //这里就是判断是把这个child添加到头部还是尾部(由LLM构造函数参数决定)
    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);
        }
    }
    //测量view宽高,这里的测量是LLM封装的一个测量宽高并加上margin的结果
    measureChildWithMargins(view, 0, 0);
    //记录当前child 消费了多少空间
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    int left, top, right, bottom;
    //从这里看到定义了left, top, right, bottom,就知道肯定是
    //在计算当前child应该摆放在屏幕的什么坐标上了
    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();
}

用伪代码来概括fill()就是

fun fill(){
    var couldLayoutSpace = layoutState.mAvailable + layoutState.mExtraFillSpace
    while (couldLayoutSpace >0 && curLayoutChildPosition < adapter.itemCount - 1){

        val layoutChunkResult = layoutChunk()
        couldLayoutSpace -= layoutChunkResult.consumed
    }
}

fun layoutChunk(){
    val childView = getChildView()
    measureChild(childView)
    layoutChild(child)
}

这篇文章我尽可能的做到每行代码都讲解,但是有很多代码是涉及后面篇章需要介绍的,所以这里就没有过多的介绍,我的对阅读源码的理解是不能贪多,最好的方式是先理解了概念一,再理解概念二,如果在概念一还没完全理解的情况,就尝试理解概念二,概念二要是理解通了还好,要是没理解好,还会打击理解概念一的信心.如果上面有什么不对的地方烦请大佬们能够指出,如果有没介绍清楚的地方,可以提出来,只有我把Recyclerview完全讲通了,才代表我真正的消化了.

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/449768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端实现端到端测试(代码版)

端到端测试框架选取 playwright 、 cypress 、 selenium 对比 cypress使用 下载 cypress npm install cypress --save-dev package.json npm run cypress:open {"scripts": {"cypress:open": "cypress open"} }使用流程 入门官方文档 npm ru…

一本通 3.4.5 最小生成树

1348&#xff1a;【例4-9】城市公交网建设问题 【题目描述】 有一张城市地图&#xff0c;图中的顶点为城市&#xff0c;无向边代表两个城市间的连通关系&#xff0c;边上的权为在这两个城市之间修建高速公路的造价&#xff0c;研究后发现&#xff0c;这个地图有一个特点&…

SQL Server基础 第四章 select定制查询(select中的各种查询筛选条件)

本章主要介绍 select 语句查询数据的基本用法&#xff0c;其中包括查询指定字段信息、条件查询等。 目录 1、比较运算符、逻辑运算符 &#xff08;1&#xff09;查询phone大于500且不是单县的 &#xff08;2&#xff09;查询地址为烟台或者单县但是phone要大于666的 &#…

IMX6ull 之 HelloWorld Led点灯

一 GPIO点灯&#xff0c;嵌入式的helloworld 1 何为GPIO&#xff1f; GPIO只是一个CPU内提供的一种功能外设&#xff0c;CPU外部的I/O引脚会被赋予一种功能&#xff08;GPIO、UART、I2C等&#xff09;&#xff1b;该功能由CPU内外设提供&#xff0c;具体是什么功能由IOMUX…

刷题笔记4-22

目录 1.Java&#xff1a;(a,b)>Math.abs(a-3)-Math.abs(b-3)&#xff1b; 2.字符解释 3.C语言二维数组中a[i]表示ai的地址&#xff0c;而a[i]又可以表示为*&#xff08;ai&#xff09; 4.二维数组在传参时&#xff0c;必须给定列 5.软件开发&#xff1a;观察者模式 6.建…

shell脚本控制

shell脚本编程系列 处理信号 Linux利用信号与系统中的进程进行通信&#xff0c;通过对脚本进行编程&#xff0c;使其在收到特定信号时执行某些命令&#xff0c;从而控制shell脚本的操作。 Linux信号 shell脚本编程会遇到的最常见的Linux系统信号如下表所示&#xff1a; 在默…

【ros】6.ros激光雷达SLAM(建图定位)

百行业为先 &#xff0c;万恶懒为首。——梁启超 文章目录 :smirk:1. 激光SLAM:blush:2. 二维激光SLAM:satisfied:3. 三维激光SLAM &#x1f60f;1. 激光SLAM SLAM&#xff08;同步定位与地图构建&#xff09;是一种机器人感知技术&#xff0c;用于在未知环境中同时确定机器人…

java调用webservicer的方法

对于使用 Webservicer的方式&#xff0c;一般采用 Java API调用的方式。Webservicer是一个运行在浏览器中的客户端程序&#xff0c;它可以通过 Webservicer的接口来访问服务器上的服务。 使用 Java调用 Webservicer有两种方式&#xff1a; 下面是一个简单的例子&#xff1a; 2、…

零基础,零成本,部署一个属于你的大模型

前言 看了那么多chatGPT的文章&#xff0c;作为一名不精通算法的开发&#xff0c;也对大模型心痒痒。但想要部署自己的大模型&#xff0c;且不说没有算法相关的经验了&#xff0c;光是大模型占用的算力资源&#xff0c;手头的个人电脑其实也很难独立部署。就算使用算法压缩后的…

数据结构和算法学习记录——小习题-二叉树的遍历二叉搜索树

目录 二叉树的遍历 1-1 1-2 1-3 二叉搜索树 2-1 2-2 2-3 2-4 答案区 二叉树的遍历 1-1 假定只有四个结点A、B、C、D的二叉树&#xff0c;其前序遍历序列为ABCD&#xff0c;则下面哪个序列是不可能的中序遍历序列&#xff1f; .ABCD .ACDB .DCBA .DABC 1-2 对于…

最精简:windows环境安装tensorflow-gpu-2.10.1

Tensorflow 2.10是最后一个在本地windows上支持GPU的版本 1. 通过.whl文件方式安装2.创建anaconda虚拟环境3.安装对应的cuda与cudnn版本&#xff0c;local不必装cuda和cudnn4. 测试tensorflow gpu是否可用 1. 通过.whl文件方式安装 .whl文件的下载地址&#xff1a; tensorflow…

windows下使用vite创建vue项目

windows下使用vite创建vue项目 1 下载安装配置NodeJS1.1 下载1.2 安装1.3 配置1.4 npm镜像加速配置1.6 设置环境变量 2 Vite简单介绍3 Vite创建vue项目3.1 vite创建vue项目的命令3.2 vite创建vue项目步骤 1 下载安装配置NodeJS 1.1 下载 下载地址&#xff1a;https://nodejs.…

全注解下的SpringIoc 续2-bean的生命周期

spring中bean的生命周期 上一个小节梳理了一下Spring Boot的依赖注入的基本知识&#xff0c;今天来梳理一下spring中bean的生命周期。 下面&#xff0c;让我们一起看看bean在IOC容器中是怎么被创建和销毁的。 bean的生命周期大致分为四个部分&#xff1a; #mermaid-svg-GFXNEU…

数据分类分级 数据识别-识别日期类型数据

前面针对数据安全-数据分类分级方案设计做了分析讲解,具体内容可点击数据安全-数据分类分级方案设计,不再做赘述 上面图片是AI创作生成!如需咒语可私戳哦! 目录 前言需求日期格式代码日期类型数据对应正则表达式前言 要做数据分类分级,重要的是分类分级模版的合理性和数…

一致性 Hash 算法 及Java TreeMap 实现

1、一致性 Hash 算法原理 一致性 Hash 算法通过构建环状的 Hash 空间替线性 Hash 空间的方法解决了这个问题&#xff0c;整个 Hash 空间被构建成一个首位相接的环。 其具体的构造过程为&#xff1a; 先构造一个长度为 2^32 的一致性 Hash 环计算每个缓存服务器的 Hash 值&…

「C/C++」C++对已有的类进行扩充

博客主页&#xff1a;何曾参静谧的博客 文章专栏&#xff1a;「C/C」C/C学习 目录 相关术语一、 继承二、组合 相关术语 继承&#xff1a;继承父类后可以拥有父类对应的属性和方法。 组合&#xff1a;将类作为成员对象&#xff0c;基类可以直接调用派生类对应的属性和方法。 一…

MySQL_第08章_聚合函数

第08章_聚合函数 讲师&#xff1a;尚硅谷 - 宋红康&#xff08;江湖人称&#xff1a;康师傅&#xff09; 官网&#xff1a; http://www.atguigu.com 我们上一章讲到了 SQL 单行函数。实际上 SQL 函数还有一类&#xff0c;叫做聚合&#xff08;或聚集、分组&#xff09;函…

59 openEuler 22.03-LTS 搭建MySQL数据库服务器-软件介绍和配置环境

文章目录 59 openEuler 22.03-LTS 搭建MySQL数据库服务器-软件介绍和配置环境59.1 软件介绍59.2 配置环境59.2.1 关闭防火墙并取消开机自启动59.2.2 修改SELINUX为disabled59.2.3 创建组和用户59.2.4 创建数据盘59.2.4.1 方法一&#xff1a;在root权限下使用fdisk进行磁盘管理5…

JVM笔记 —— 垃圾回收(GC)详解

一、垃圾回收的分类 针对HotSpot JVM的实现&#xff0c;它里面的GC其实准确分类只有两大种: Partial GC&#xff1a;部分收集模式 Young GC&#xff1a;只收集年轻代的GCOld GC&#xff1a;只收集老年代的GC。只有CMS中有这个模式。Mixed GC&#xff1a;收集整个年轻代以及部分…

mybatis的基本使用和理解

mybatis的基本使用和理解 Lombok的使用(使用注解的方式将实体类中的get、set、构造函数代替&#xff09; Lombok是一个Java库&#xff0c;能自动插入编辑器并且构建工具&#xff0c;简化Java开发。通过添加注解的方式&#xff0c;不需要为类编写getter或equals方法&#xff0…