美文网首页
RecyclerView 源码分析(二):布局

RecyclerView 源码分析(二):布局

作者: MrFengZH | 来源:发表于2019-08-06 15:18 被阅读0次

    前言

    本文将主要分析 RecyclerView 的布局过程(以第一次布局为例),由于 RecyclerView 的 measure 也与 layout 关系比较密切,所以接下来主要看 onMeasure 和 onLayout,先看 onMeasure:

    onMeasure

       @Override
        protected void onMeasure(int widthSpec, int heightSpec) {
            // mLayout 是 LayoutManager,通过 setLayoutManager 方法设置,没有设置则为空
            if (mLayout == null) {
                defaultOnMeasure(widthSpec, heightSpec);
                return;
            }
            // 是否开启自动测量(RV 提供的几种 LM 都开启了自动测量)
            if (mLayout.isAutoMeasureEnabled()) {
                final int widthMode = MeasureSpec.getMode(widthSpec);
                final int heightMode = MeasureSpec.getMode(heightSpec);
                
                // 实际上就是调用 RV 的 defaultOnMeasure 方法
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    
                final boolean measureSpecModeIsExactly =
                        widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
                // 如果测量模式是 EXACTLY,退出
                if (measureSpecModeIsExactly || mAdapter == null) {
                    return;
                }
    
                // 布局状态为 STEP_START 时,进行 step1
                if (mState.mLayoutStep == State.STEP_START) {
                    dispatchLayoutStep1();
                }
    
                mLayout.setMeasureSpecs(widthSpec, heightSpec);
                mState.mIsMeasuring = true;
                // 进行 step2
                dispatchLayoutStep2();
    
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    
                // 判断是否要测量两次
                if (mLayout.shouldMeasureTwice()) {
                    mLayout.setMeasureSpecs(
                            MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                            MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                    mState.mIsMeasuring = true;
                    dispatchLayoutStep2();
                    mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
                }
            } else {
                // ...
            }
        }
    

    RecyclerView 重写了 onMeasure 方法。该方法中,先判断是否有设置 LayoutManager,没有设置就执行 defaultOnMeasure。

    设置了 LayoutManager 的话,就要判断 LayoutManager 是否开启了自动测量,开启的话就会使用默认的测量机制,否则就需要通过 LayoutManager 的 onMeasure 方法来完成测量工作。系统提供的几个 LayoutManager 都开启了自动测量。

    自动测量时,涉及到一个重要的类:RecyclerView.State,这个类封装了当前 RecyclerView 的状态信息。其 mLayoutStep 变量表示当前 RecyclerView 的布局状态,状态有三种:

    • STEP_START
    • STEP_LAYOUT
    • STEP_ANIMATIONS

    一开始的状态为 STEP_START,调用完 dispatchLayoutStep1 方法后,状态变为 STEP_LAYOUT,表示接下来要进行布局,调用完 dispatchLayoutStep2 方法后,状态变为 State.STEP_ANIMATIONS,等待之后在 layout 时执行 dispatchLayoutStep3

    这三个 step 负责不同的工作,step1 负责更新和记录状态,step2 真正进行布局,step3 执行动画并进行清理工作。

    可以看到,在开启自动测量时,RecyclerView 如果是 WRAP_CONTENT 状态,就要根据子 View 所占空间大小动态调整自己的大小,这时它就将子 View 的 measure 和 layout 提前到 onMeasure 中,因为它需要确定子 View 的大小和位置后,再来设置自己的大小。所以就会在 onMeasure 中执行 step1 和 step2。

    接下来看一下 RecyclerView 的 layout 过程:

    onLayout

        @Override
        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() {
            // ...
            
            mState.mIsMeasuring = false;
            // 如果已经在 onMeasure 执行了 step1 和 step2,就不再执行 step1
            // 至于 step2,如果发现尺寸发生了改变,将会再执行一次
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                    || mLayout.getHeight() != getHeight()) {
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else {
                mLayout.setExactMeasureSpecsFrom(this);
            }
            dispatchLayoutStep3();
        }
    

    可以看到,如果已经在 onMeasure 执行了 step1 和 step2,就不再执行 step1,至于 step2,如果发现尺寸发生了改变,将会再执行一次,否则也不会执行。最后执行 step3。

    下面分别看下这 3 个 step,首先看 step1

    RecyclerView#dispatchLayoutStep1

        private void dispatchLayoutStep1() {
            // ...
            
            // (1)
            processAdapterUpdatesAndSetAnimationFlags();
    
            // (2)
            if (mState.mRunSimpleAnimations) {
                // ...
            }
            if (mState.mRunPredictiveAnimations) {
                // ...
            }
    
            // ...
            mState.mLayoutStep = State.STEP_LAYOUT;
        }
    

    先看注释(2),这里会根据 mRunSimpleAnimations 和 mRunPredictiveAnimations 的值来决定是否运行简单动画和预动画。这两个值是在哪里设置的呢?答案是在注释(1)的 processAdapterUpdatesAndSetAnimationFlags 方法处:

    RecyclerView#processAdapterUpdatesAndSetAnimationFlags

        private void processAdapterUpdatesAndSetAnimationFlags() {
            // ...
            
            // mItemsAddedOrRemoved:当有 item 添加或删除的时候设置为 ture
            // mItemsChanged:当有 item 的数据更新时设置为 true
            boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
            
            // 1. mFirstLayoutComplete:第一次 layout 完成后,设置为 true
            // 2. mItemAnimator:默认为 DefaultItemAnimator,可通过 RecyclerView 的 setItemAnimator 方法设置
            // 3. mDataSetHasChangedAfterLayout:调用 setAdapter、swapAdapter 或 notifyDateSetChanged 
            // 后设置为 true,在 layout 过程的 step3 中设置为 false
            // 4. mLayout.mRequestedSimpleAnimations:默认为 false,
            // 可以通过调用 LayoutManager 的 requestSimpleAnimationsInNextLayout 方法将该值设置为 true
            // 5. mAdapter.hasStableIds:默认为 false,可通过 Adapter 的 setHasStableIds 方法设置
            mState.mRunSimpleAnimations = mFirstLayoutComplete
                    && mItemAnimator != null
                    && (mDataSetHasChangedAfterLayout
                    || animationTypeSupported
                    || mLayout.mRequestedSimpleAnimations)
                    && (!mDataSetHasChangedAfterLayout
                    || mAdapter.hasStableIds());
                    
            // predictiveItemAnimationsEnabled:LinearLayoutManager 默认支持预动画,返回 true
            mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                    && animationTypeSupported
                    && !mDataSetHasChangedAfterLayout
                    && predictiveItemAnimationsEnabled();
        }
    

    里面的一些属性在注释中已经有说明。这里以第一次 layout 为例,此时由于第一次 layout 过程还未完成,mFirstLayoutComplete 为 false,mRunSimpleAnimations 也就为 false,进而 mRunPredictiveAnimations 也为 false。

    所以在第一次 layout 中,并不会进行简单动画和预动画。这里就先不分析了,详细过程在分析动画的时候再说。

    下面重点看一下 step2:

    RecyclerView#dispatchLayoutStep2

        private void dispatchLayoutStep2() {
            // ...
    
            mLayout.onLayoutChildren(mRecycler, mState);
    
            // ...
        }
    

    step2 进行真正的布局,布局任务交由 LayoutManager 负责,调用其 onLayoutChildren 方法为所有子 View 布局。该方法交由具体的 LayoutManager 实现,这里以 LinearLayoutManager 为例,看一下它的 onLayoutChildren 实现:

    LinearLayoutManager#onLayoutChildren

        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            // ...
    
            // AnchorInfo 的 mValid 属性默认为 false 
            if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                    || mPendingSavedState != null) {
                mAnchorInfo.reset();
                // mShouldReverseLayout 和 mStackFromEnd 默认都为 false
                // 异或操作后结果仍为 false
                mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
                // 找到锚点的位置,保存到 AnchorInfo 的 mPosition 中
                updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
                mAnchorInfo.mValid = true;
            }
    
            // ...
            
            // (1)
            detachAndScrapAttachedViews(recycler);
            
            if (mAnchorInfo.mLayoutFromEnd) {
                // ...
            } else {
                // fill towards end
                updateLayoutStateToFillEnd(mAnchorInfo);
                mLayoutState.mExtra = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                // ...
    
                // fill towards start
                updateLayoutStateToFillStart(mAnchorInfo);
                mLayoutState.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false);
    
                // ...
            }
            
            // ...
        }
    

    注释(1)处调用了 detachAndScrapAttachedViews 方法,该方法会将子 View 移除并根据情况添加到相应缓存中。所以如果不是第一次 layout,RecyclerView 已经存在子 View 的话,在重新填充布局前,会将旧的子 View 添加到缓存中,这样之后填充布局时就可以直接从缓存中拿,不用再次创建子 View。

    下面看下布局过程,主要分两步:

    1. 找到锚点(auchor 点)

    该过程通过 updateAnchorInfoForLayout 方法实现:

    updateAnchorInfoForLayout

        private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
                AnchorInfo anchorInfo) {
            // 一般这里都是返回 false
            if (updateAnchorFromPendingData(state, anchorInfo)) {
                return;
            }
            
            // 首先从子 View 中获取锚点
            if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
                return;
            }
    
            // 没有从子 View 得到锚点,就将头或尾设置为锚点(默认将头设置为锚点)
            anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
        }
    

    继续看下 updateAnchorFromChildren 方法,该方法从子 View 中获取锚点

    updateAnchorFromChildren

        private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
                RecyclerView.State state, AnchorInfo anchorInfo) {
            if (getChildCount() == 0) {
                return false;
            }
            // 将被 focus 的子 View 作为锚点
            final View focused = getFocusedChild();
            if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
                anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
                return true;
            }
            
            if (mLastStackFromEnd != mStackFromEnd) {
                return false;
            }
            
            // 根据 layout 的方向决定锚点,默认从上往下,所以锚点在头部
            View referenceChild = anchorInfo.mLayoutFromEnd
                    ? findReferenceChildClosestToEnd(recycler, state)
                    : findReferenceChildClosestToStart(recycler, state);
            if (referenceChild != null) {
                anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
                // ...
                return true;
            }
            return false;
        }
    

    可以看到,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认从上往下布局,所以锚点选取头部。

    如果想要从下往上布局,可以这样设置:

        linearLayoutManager.setStackFromEnd(true);
    

    这样的话,锚点会在尾部,数据加载完后首先显示的是底部的数据。

    2. 填充布局

    根据布局方向,先后填充满锚点上方和下方的所有区域

    填充的过程调用 fill 方法:

    fill

        int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
                RecyclerView.State state, boolean stopOnFocusable) {
            // ...
            
            // 进行 layout 时,layoutState.mScrollingOffset 的值等于 
            // LayoutState.SCROLLING_OFFSET_NaN,不会进入此 if 块,这里先不分析
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                // ...
                // 进行回收工作
                recycleByLayoutState(recycler, layoutState);
            }
            
            int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
            LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
            while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
                // ...
                
                // (1)
                layoutChunk(recycler, state, layoutState, layoutChunkResult);
    
                // ...
            }
            // ...
        }
    

    看注释(1)处,在 while 循环里有一个 layoutChunk 方法,只要还有剩余空间,就不会不断执行该方法:

    layoutChunk

        void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                LayoutState layoutState, LayoutChunkResult result) {
            // (1)
            View view = layoutState.next(recycler);
    
            // ...
            
            // 默认情况下,layoutState.mScrapList 等于 null
            if (layoutState.mScrapList == null) {
                // mShouldReverseLayout 默认为 false,可通过 LLM 的 setReverseLayout 方法设置
                // 从上往下填充布局时,layoutState.mLayoutDirection 为 LayoutState.LAYOUT_END
                // 默认情况下,从上往下布局时进入 if 块
                if (mShouldReverseLayout == (layoutState.mLayoutDirection
                        == LayoutState.LAYOUT_START)) {
                    // (2)
                    addView(view);
                } else {
                    addView(view, 0);
                }
            } else {
                // ...
            }
    
            // ...
            
            // 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);
    
        }
    

    先看注释(1)处,这里返回下一个要填充的 View,来看下具体过程:

        View next(RecyclerView.Recycler recycler) {
            // ...
            
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
    

    可以看到,获取 View 的工作也是交给了 Recycler,通过 Recycler 的 getViewForPosition 来获取一个指定位置的子 View,该方法在 Recycler 已经分析过了。

    继续看注释(2)处的 addView 方法:

        private void addViewInt(View child, int index, boolean disappearing) {
            final ViewHolder holder = getChildViewHolderInt(child);
            
            // ...
            
            // 该 ViewHolder 从 ChangedScrap、AttachedScrap、HiddenViews 中得到
            // 或者该 ViewHolder 曾经通过 scrapView 方法缓存到 Scrap 缓存中 
            if (holder.wasReturnedFromScrap() || holder.isScrap()) {
                // 做些清理工作:删除 Scrap 缓存、清除标记等
                if (holder.isScrap()) {
                    holder.unScrap();
                } else {
                    holder.clearReturnedFromScrapFlag();
                }
                // 子 View 重新 attach 到 RecyclerView 中
                mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
                // DISPATCH_TEMP_DETACH:该值默认为 false,且没看到有地方将其设置为 true
                if (DISPATCH_TEMP_DETACH) {
                    ViewCompat.dispatchFinishTemporaryDetach(child);
                }
            } 
            // 该子 View 一直是有效的,只是可能要移动下位置(对应滑动时没有滑出屏幕的子 View)
            else if (child.getParent() == mRecyclerView) {
                int currentIndex = mChildHelper.indexOfChild(child);
                if (index == -1) {
                    index = mChildHelper.getChildCount();
                }
                // 将该子 View 移动到正确位置
                if (currentIndex != index) {
                    mRecyclerView.mLayout.moveView(currentIndex, index);
                }
            } 
            // 其他情况,例如从 CahcedView 或 RecycledViewPool 得到的缓存 View,或者是新创建的 View
            else {
                mChildHelper.addView(child, index, false);
                lp.mInsetsDirty = true;
                if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
                    mSmoothScroller.onChildAttachedToWindow(child);
                }
            }
            
            // ...
        }
    

    该方法通过判断 View 的来源,利用不同的方式将子 View 添加到 RecyclerView 中,填充完布局。

    最后看一下 step3:

    dispatchLayoutStep3

        private void dispatchLayoutStep3() {
            // ...
            
            // 将 layout 状态重置回 State.STEP_START
            mState.mLayoutStep = State.STEP_START;
            
            // 执行动画
            if (mState.mRunSimpleAnimations) {
                // ...
            }
    
            // 清空 attachedScrap
            mLayout.removeAndRecycleScrapInt(mRecycler);
            // 重置一系列的变量
            mState.mPreviousLayoutItemCount = mState.mItemCount;
            mDataSetHasChangedAfterLayout = false;
            mDispatchItemsChangedEvent = false;
            mState.mRunSimpleAnimations = false;
    
            mState.mRunPredictiveAnimations = false;
            mLayout.mRequestedSimpleAnimations = false;
            // 清空 changedScrao
            if (mRecycler.mChangedScrap != null) {
                mRecycler.mChangedScrap.clear();
            }
    
            // 其它清理工作
        }
    

    step3 主要是执行动画和进行一系列的清理工作,例如重置 layout 状态,清理 Scrap 缓存等等。由于在第一次布局时,mState.mRunSimpleAnimations 为 false,不会执行动画,动画部分就先不分析了。

    小结

    前面说了这么多,这里小结一下 onLayout 的过程:

    1. layout 过程分为 3 个 step,step1 负责更新和记录状态,step2 真正进行布局,step 执行动画并进行清理工作。如果 RecyclerView 的宽高为 WRAP_CONTENT 模式,那么需要在 measure 过程提前进行 step1 和 step2,先获得子 View 的大小,才能确定自己的大小。而 step3 肯定是在 layout 过程执行。
    2. step2 真正进行布局,布局任务由 LayoutManager 负责,通过它的 onLayoutChildren 方法对子 View 进行布局。布局过程分两步:
      1. 找到锚点,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认头部为锚点。
      2. 根据布局方向,先后填充满锚点上方和下方的区域,填充所需的 View 交由 Recycler 提供。

    写在最后

    本文主要以第一次布局,分析了 RecyclerView 的 measure 和 layout 过程。当然,主要分析的还是 layout 过程。至于第二次 layout 或者是更新列表时的 layout,会在动画和缓存上有所不同,但主要流程还是一样的,并且缓存相关的在第一篇有更详细的说明,而动画的话可能会在后面另开一篇来讲。

    参考

    相关文章

      网友评论

          本文标题:RecyclerView 源码分析(二):布局

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