RecyclerView源码浅析之测量与布局

概述

源码一万多行,是ViewPager的三倍多,如果不能把握住主线,会很容易迷失在源码的海洋。RecyclerView做得比ViewPager好的地方在于,它进行良好的封装和解耦,每一个类完成自己的功能,结构十分清晰。但是对于我们来说,如果不能对这些模块先有一个大概的的了解,那么阅读源码的常常会因为不清楚一个类的作用而不知所云。所以这篇文章的主线是解读RecyclerView的测量、布局和绘制,同时会对涉及的类做一个大概的介绍。

我们还是按照一般在Activity或Fragment中调用的API来尝试分析主线流程。一般我们会使用如下的代码(先不看Decoration和Animator相关):

    mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    //LayoutManager
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    layoutManager.setOrientation(OrientationHelper. VERTICAL);
    recyclerView.setLayoutManager(layoutManager);  
    //Adapter
    mAdapter = new MyAdapter(...);
    mRecyclerView.setAdapter(mAdapter);
    //Decoration
    mRecyclerView.addItemDecoration( new DividerGridItemDecoration(this ));
    //Animator
    mRecyclerView.setItemAnimator( new DefaultItemAnimator());

构造器中初始化了一些数据,这里重点注意下AdapterHelper和ChildHelper两个类,后面我们再说。

       public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            ...
            mItemAnimator.setListener(mItemAnimatorListener);
            //初始化AdapterHelper
            initAdapterManager();
            //初始化ChildrenHelper
            initChildrenHelper();
            ...

        }

看到setLayoutManager(),重新设置LayoutManager,并请求重绘。重绘的请求交到主Handler中等待执行。

        public void setLayoutManager(LayoutManager layout) {
            if (layout == mLayout) {
                return;
            }
            stopScroll();
            //清除工作
            ...
            // this is just a defensive measure for faulty item animators.
            mChildHelper.removeAllViewsUnfiltered();
            mLayout = layout;
            if (layout != null) {
                //持有RecyclerView
                mLayout.setRecyclerView(this);
                if (mIsAttached) {
                    mLayout.dispatchAttachedToWindow(this);
                }
            }
            mRecycler.updateViewCacheSize();
            //请求重绘
            requestLayout();
        }

一般我们会新建LinearLayoutManager或者StaggeredGridLayoutManager,注意这里每个LayoutManager会有一个成员mAutoMeasure,这个变量在初始化的时候被设为true,会影响到后面RecyclerView的测量。

        //LinearLayoutManager.java
        public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
            setOrientation(orientation);
            setReverseLayout(reverseLayout);
          //置为true
            setAutoMeasureEnabled(true);
        }
    //StaggeredGridLayoutManager.java
    public StaggeredGridLayoutManager(int spanCount, int orientation) {
            mOrientation = orientation;
            setSpanCount(spanCount);
            setAutoMeasureEnabled(mGapStrategy != GAP_HANDLING_NONE);
            mLayoutState = new LayoutState();
            createOrientationHelpers();
        }

继续看到setAdapter(),最终会调用到setAdapterInternal()

        private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
                boolean removeAndRecycleViews) {
            //注销监听者,也就是RecyclerView的一个内部类实例
            if (mAdapter != null) {
                mAdapter.unregisterAdapterDataObserver(mObserver);
                mAdapter.onDetachedFromRecyclerView(this);
            }
            if (!compatibleWithPrevious || removeAndRecycleViews) {
                //清空所有View,包括已添加的和Recycler中的
                removeAndRecycleViews();
            }
            //将AdapterHelper中数据清空
            mAdapterHelper.reset();
            final Adapter oldAdapter = mAdapter;
            mAdapter = adapter;
            if (adapter != null) {
                //Adapter作为Observable注册Observer
                adapter.registerAdapterDataObserver(mObserver);
                adapter.onAttachedToRecyclerView(this);
            }
            if (mLayout != null) {
                mLayout.onAdapterChanged(oldAdapter, mAdapter);
            }
            mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
            mState.mStructureChanged = true;
            //让所有View、ViewHolder、Decorator变为无效
            markKnownViewsInvalid();
        }

onMeasure()

构造器、setLayoutManager()、setAdapter()做了一些“准备”工作,接下来RecycleView会进入绘制流程。

进入绘制流程后,ViewRootImpl会下发measure、layout和draw过程。我debug了onMeasure和onLayout,下面从整体上来说明下RecyclerView的子View测量布局过程,尽量做到详略得当。后面都以LinearLayoutManager为例,不再说明。

首先是onMeasure()。这里mLayout.mAutoMeasure = true(如果为false,那么你的自定义LayoutManager要重写onMeasure()方法)。由于我们一般情况下我们的RecyclerView的宽高是MATCH_PARENT,所以在为RecyclerView设置了一个宽高后,跳出onMeasure()。如果宽高是Wrap_Content,那么会走接下来的流程,由于接下来的流程和onLayout()中有重复,所以我说下大致的意思:先执行dispatchLayoutStep2()方法添加子View、测量子View、布局子View,然后调用mLayout.setMeasuredDimensionFromChildren()根据子View设置自己的宽高,这个逻辑也很好理解,23.2.0之后支持了RecyclerView的Wrap_Content属性。

只对本身进行了一个宽高设置后,子View也没有测量我们就跳出了onMeasure()函数,看来onLayout()中就是问题的关键了。

        @Override
        protected void onMeasure(int widthSpec, int heightSpec) {
            ...
              //会进入这个分支
            if (mLayout.mAutoMeasure) {
                final int widthMode = MeasureSpec.getMode(widthSpec);
                final int heightMode = MeasureSpec.getMode(heightSpec);
                final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                        && heightMode == MeasureSpec.EXACTLY;
              //把宽高先设置成了EXACTLY模式下的宽高
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
              //注意,如果我们的RecyclerView的宽高是MATCH_PARENT的话,这里直接结束onMeasure
                if (skipMeasure || mAdapter == null) {
                    return;
                }
                if (mState.mLayoutStep == State.STEP_START) {
                    dispatchLayoutStep1();
                }
                mLayout.setMeasureSpecs(widthSpec, heightSpec);
                mState.mIsMeasuring = true;
              //进行子View的相关工作
                dispatchLayoutStep2();
                // 根据子View重新测量自身,这里是为了支持Wrap_Content
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
                ...
            }
            ...
        }

onLayout()

我们直接进入dispatchLayout()。

        @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;
        }

总体上来看一共有三个步骤dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3(),每一个函数执行完后会修改mState.mLayoutStep(有3个状态)为下一个步骤的状态。

       void dispatchLayout() {
            ...

            if (mState.mLayoutStep == State.STEP_START) {
                //步骤1
                dispatchLayoutStep1();
                mLayout.setExactMeasureSpecsFrom(this);
                //步骤2
                dispatchLayoutStep2();
            } 
            ...
            //步骤3
            dispatchLayoutStep3();
            onExitLayoutOrScroll();
        }
  • State
    注释给的解释是这个类包含了当前RecyclerView状态的有用信息,包括滚动的最终位置或者焦点等。
    这个类我理解为RecyclerView为各个子模块提供的上下文 , 它保存了一些RecyclerView当前需要的状态,并且在子模块需要这些信息的时候,传递State实例过去。这里要说一个重要的思想:数据集中的count和界面中的count不一定相等,数据集发生变化后,RecyclerView通过Layout达成界面和数据集的同步,也因此会产生动画。
    一些重要的解释注释在代码中。

         public static class State {
                  ...
                //如果在滑动,记录的是重点position
                  private int mTargetPosition = RecyclerView.NO_POSITION;
                //当前处于哪个Layout阶段,还有STEP_LAYOUT,STEP_ANIMATIONS,从名字也可以看出,第二个阶段是真正布局的阶段
                  private int mLayoutStep = STEP_START;
    
                  private SparseArray<Object> mData;
                  //对应于RecyclerView中Item的数量
                //和LayoutManager中的getItemCount返回的数量不太一样,LM返回的数量一定是数据集的数量
                  int mItemCount = 0;
                //上一次Layout后Item的数量(因为Layout后数据集中的数据就和界面同步了)
                  private int mPreviousLayoutItemCount = 0;
                  //在prelayout阶段从adpter中删除的item的数量
                  private int mDeletedInvisibleItemCountSincePreviousLayout = 0;
                //是否有结构变化(指增删,更新不算)
                  private boolean mStructureChanged = false;
                //是否在prelayout阶段
                  private boolean mInPreLayout = false;
                //支持的动画模式
                  private boolean mRunSimpleAnimations = false;
                  private boolean mRunPredictiveAnimations = false;
                //是否正在测量中
                  private boolean mIsMeasuring = false;
                ...
                 //Returns the total number of items that can be laid out.
                   //返回的是数据集中的数量
                  public int getItemCount() {
                      return mInPreLayout ?
                              (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) :
                              mItemCount;
                  }
    
              }

dispatchLayoutStep2()

我们先略去dispatchLayoutStep1()和dispatchLayoutStep3(),单纯来看下RecyclerView是如何布局子View的。(其实是因为1和3目前还没有搞得很清楚)

    private void dispatchLayoutStep2() {
        //RecyclerView改写了requestLayout(),调用这个方法后,布局的时候调用addView添加子View并不会引起新一轮绘制流程了
        eatRequestLayout();
      ...
        //获取数据集中个数
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        //重点
        mLayout.onLayoutChildren(mRecycler, mState);
        ...
    }

转入LinearLayoutManager的onLayoutChildren(),在这之前先说两个子模块LayoutState和OrientationHelper。

  • LayoutState
    保存了布局时所需要的信息,随着布局的进行信息将会更新,布局完成后将被清空。重要的内容注释在代码中

          static class LayoutState {
                //方向
                  final static int LAYOUT_START = -1;
                  final static int LAYOUT_END = 1;
                //布局开始的地方,会随着不断布局子View更新
                  int mOffset;
                //在布局方向上还需布局的距离(Pixels)
                  int mAvailable;
                //下一个数据在数据集中的position
                  int mCurrentPosition;
                //数据集遍历的方向
                  int mItemDirection;
                //这次layout的方向
                  int mLayoutDirection;
                //在scrolling状态下被构造,代表不创建新View的情况下可以滑动多少距离
                  int mScrollingOffset;
                //用于prelayout
                  int mExtra = 0;
                  boolean mIsPreLayout = false;
                //上一次滑动的距离
                  int mLastScrollDelta;
                //用于LLM想布局特殊的View,从这个List获取
                  List<RecyclerView.ViewHolder> mScrapList = null;
    
                  boolean mInfinite;
                //数据集中是否还有更多数据
                  boolean hasMore(RecyclerView.State state) {
                      return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
                  }
                //很关键的方法,向Recycler索取一个View,由于解耦得非常好,所以这里我们只需要理解为能根据mCurrentPosition和ViewType获取一个View就可以,至于怎么获得,在下一篇再说
                  View next(RecyclerView.Recycler recycler) {
                      if (mScrapList != null) {
                          return nextViewFromScrapList();
                      }
                      final View view = recycler.getViewForPosition(mCurrentPosition);
                      mCurrentPosition += mItemDirection;
                      return view;
                  }
                ...
              }
  • OrientationHelper
    Linearlayout的布局方向有两个,这个类把布局方向封装,内部实现了对方向的转换,即转换成了方向无关的“Start”和“End”,以方便布局的时候使用。我们看一下“纵向的Helper”。可以看到对外提供了很方便的接口。

          public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
                  return new OrientationHelper(layoutManager) {
                      @Override
                      public int getEndAfterPadding() {
                          return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
                      }
    
                      @Override
                      public int getEnd() {
                          return mLayoutManager.getHeight();
                      }
    
                      @Override
                      public void offsetChildren(int amount) {
                          mLayoutManager.offsetChildrenVertical(amount);
                      }
    
                      @Override
                      public int getStartAfterPadding() {
                          return mLayoutManager.getPaddingTop();
                      }
    
                      @Override
                      public int getDecoratedMeasurement(View view) {
                          final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                                  view.getLayoutParams();
                          return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
                                  + params.bottomMargin;
                      }
    
                      @Override
                      public int getDecoratedMeasurementInOther(View view) {
                          final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                                  view.getLayoutParams();
                          return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
                                  + params.rightMargin;
                      }
    
                      @Override
                      public int getDecoratedEnd(View view) {
                          final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                                  view.getLayoutParams();
                          return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
                      }
    
                      @Override
                      public int getDecoratedStart(View view) {
                          final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                                  view.getLayoutParams();
                          return mLayoutManager.getDecoratedTop(view) - params.topMargin;
                      }
                ...
    
                      @Override
                      public void offsetChild(View view, int offset) {
                          view.offsetTopAndBottom(offset);
                      }
    
                      @Override
                      public int getEndPadding() {
                          return mLayoutManager.getPaddingBottom();
                      }
    
                     ...
                  };
              }

看完了这两个子模块,可以真正进入布局的过程了,注释也写了,主要有4步:找到锚点,向start去fill,向end去fill,scroll以满足要求。我留下一些关键的步骤并注释。

    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

      //如果mPendingSavedState不空 或者 mPendingScrollPosition != NO_POSITION 就回收所有View
      //mPendingSavedState和状态保存有关,会保存和恢复锚点相关信息
      //mPendingScrollPosition代表scroll to a position时会设置这个值并且触发layout
        if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
            if (state.getItemCount() == 0) {
             //这里回收的是 已经添加到RV中且不是hidden的子View(hidden先理解为要删除但正在播放动画的View)
              //至于回收的定义是什么,回收时做了什么,下篇再说,这里Recycler已经解耦得很好,只需要知道被回收
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
      //从状态保存的信息中恢复锚点
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
        }
      //新建State和OrientationHelper
        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // resolve layout direction
        resolveShouldLayoutReverse();
        //重置锚点信息,AnchorInfo也是一个数据结构,保存了一些简单的信息
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        //更新锚点信息,可能从3种方式回去锚点,这里第一次布局直接从0或state.getItemCount() - 1开始
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        //如果有滑动的目标position,那么LLM会额外布局一些空间(应该是提升用户体验)
        int extraForStart;
        int extraForEnd;
      //从源码来看,这里获取的是出去padding的height,也就是大约一页面的距离
        final int extra = getExtraLayoutSpace(state);
        //如果上一次是内容向下滑动,那么这一次额外布局应该接在末尾
        if (mLayoutState.mLastScrollDelta >= 0) {
            extraForEnd = extra;
            extraForStart = 0;
        } else {
            extraForStart = extra;
            extraForEnd = 0;
        }
      //考虑padding
        extraForStart += mOrientationHelper.getStartAfterPadding();
        extraForEnd += mOrientationHelper.getEndPadding();
       ...//没看懂的但不影响大局的删除了
       //回调
        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
      //这里又有一种回收,我们先不管,只需要知道也是针对于和上面一样的“可见的”View就行了
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mIsPreLayout = state.isPreLayout();
      //真正开始布局
        if (mAnchorInfo.mLayoutFromEnd) {
            ...
        } else {
          //由于是从锚点向上下fill,我们只看一半就好了
            // fill towards end
          //更新State准备向下fill
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
          //关键函数,后面单独拉出
            fill(recycler, mLayoutState, state, false);
            ...
            // fill towards start
            ...
        }

        //填补可能产生的gap
        if (getChildCount() > 0) {
            ...
        }

    }

整个流程看完了,梳理一下无非是:更新了锚点信息,可能计算extra,回收View,然后就开始从锚点fill。那么这个fill函数就是重点,我们单独拉出来看一下。整个函数主要做了这么一件事:在循环中layoutChunk(),更新信息以及回收超出边界的View。

   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;
      //回收滑出边界的View(同样回收逻辑忽略)
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_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 = new LayoutChunkResult();
     //如果还有剩余空间且可以获得更多的View
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
           //关键函数,等下解释
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {
                break;
            }
          //更新Offset
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            //更新.mAvailable和remainingSpace
            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;
            }
            //回收超出边界的子View
            if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
        }
        return start - layoutState.mAvailable;
    }

我们来看这个方法,终于看到了子View 相关的动作:从State中获取一个View,添加,测量,布局。

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
      //获取一个View
        View view = layoutState.next(recycler);
        ...
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
              //添加View,注意这里会引起RV的重绘,但是记得上面在讲Step2的时候有一个 eatRequestLayout()
              //会阻止requestlayout请求,也就是说循环添加子View的时候并不会触发RV的重绘。
              //还要说一下,这里的在ChildHelper中注册了信息然后添加到了ViewGroup中
                addView(view);
            } else {
                addView(view, 0);
            }
        } 
        //测量
        measureChildWithMargins(view, 0, 0);
        ...
       //布局
        layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
                right - params.rightMargin, bottom - params.bottomMargin);

        。。。
    }

小结

看到这里,step2算是结束了,step3涉及到动画,不影响理解RV的绘制流程,这里先打住。

总结一下,初次布局,如果是Match_Parent,会直接跳出onMeasure()进入onLayout()(如果是wrap_content,那么要先进行子View的测量布局,然后确定自身大小)。onLayout中分为3步,step2为真正对子View进行操作的地方,流程大致是找到锚点,以锚点为基准获取子View,添加子View,测量子View,布局子View,同时还有可能回收超出边界的View。之后的onDraw方法就不再说了。

RV的绘制流程算是走完了,但还有很多问题没有解决:step1和3到底做了什么,动画是如何产生的,ChildHelper与AdapterHelper的功能是什么,回收机制是什么样的,从Recycler获取View又是什么样的流程。

这些问题我弄清楚整合以后会在后面几篇给出我的见解~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值