RecyclerView中根据其功能可以分为以下几个模块:
protected void onMeasure(int widthSpec, int heightSpec) {//mLayout就是之前说过的LayoutManagerif (mLayout == null) {// 第一种情况}if (mLayout.isAutoMeasureEnabled()) {// 第二种情况} else {// 第三种情况}
}
在onMeasure方法中,recyclerView根据mLayout分为了三种情况,接下来会对这三种情况进行梳理。
第一种,当mLayout为null的时候:
if (mLayout == null) {defaultOnMeasure(widthSpec, heightSpec);return;
}
void defaultOnMeasure(int widthSpec, int heightSpec) {// calling LayoutManager here is not pretty but that API is already public and it is better// than creating another method since this is internal.final int width = LayoutManager.chooseSize(widthSpec,getPaddingLeft() + getPaddingRight(),ViewCompat.getMinimumWidth(this));final int height = LayoutManager.chooseSize(heightSpec,getPaddingTop() + getPaddingBottom(),ViewCompat.getMinimumHeight(this));setMeasuredDimension(width, height);
}
public static int chooseSize(int spec, int desired, int min) {final int mode = View.MeasureSpec.getMode(spec);final int size = View.MeasureSpec.getSize(spec);switch (mode) {//表示精确模式,View的大小已经确认,为SpecSize所指定的值case View.MeasureSpec.EXACTLY:return size;//指定了最大大小case View.MeasureSpec.AT_MOST:return Math.min(size, Math.max(desired, min));//父容器不对子View有限制,子View要多大给多大case View.MeasureSpec.UNSPECIFIED:default:return Math.max(desired, min);}
}
可以看到在mLayout为Null的情况下,recyclerView还是做了测量操作,但是由于在onLayout方法中跳过了layout,因此不会展示任何东西。
if (mLayout == null) {Log.e(TAG, "No layout manager attached; skipping layout");// leave the state in STARTreturn;
}
在这种情况下,RecyclerView会先后调用dispatchLayoutStep1()和dispatchLayoutStep2()方法,除此之外,还有一个dispatchLayoutStep3()方法会在onLayout中进行调用,这三个方法对应了RecyclerView的三种不同状态,State.STEP_START、State.STEP_LAYOUT和State.STEP_ANIMATIONS。
State.STEP_START表示RecyclerView还未经历dispatchLayoutStep1()。
State.STEP_LAYOUT表示此时处于layout阶段,这个阶段会调用dispatchLayoutStep2方法layout RecyclerView的children。调用dispatchLayoutStep2方法之后,此时mState.mLayoutStep变为了State.STEP_ANIMATIONS。
当mState.mLayoutStep为State.STEP_ANIMATIONS时,表示RecyclerView处于第三个阶段,也就是执行动画的阶段,也就是调用dispatchLayoutStep3方法。当dispatchLayoutStep3方法执行完毕之后,mState.mLayoutStep又变为了State.STEP_START。
具体地,先来看一下源码:
if (mLayout.isAutoMeasureEnabled()) {final int widthMode = MeasureSpec.getMode(widthSpec);final int heightMode = MeasureSpec.getMode(heightSpec);//官方说明:这里的调用已经废弃,事实上已经替换为defaultOnMeasure//也就是说mLayout应该调用defaultOnMeasure()方法//但是为了防止第三方代码被破坏,还是保留了下来//所有开发人员在isAutoMeasureEnabled为true不应该覆盖onMeasure方法mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);final boolean measureSpecModeIsExactly =widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;if (measureSpecModeIsExactly || mAdapter == null) {return;}if (mState.mLayoutStep == State.STEP_START) {dispatchLayoutStep1();}// 在第 2 步中设置尺寸。 为了保持一致性,应该使用旧尺寸进行预布局mLayout.setMeasureSpecs(widthSpec, heightSpec);mState.mIsMeasuring = true;dispatchLayoutStep2();// 现在我们可以从孩子那里得到宽度和高度.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);// 如果 RecyclerView 的宽度和高度不准确,并且至少有一个孩子的宽度和高度也不准确,我们必须重新测量。if (mLayout.shouldMeasureTwice()) {mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));mState.mIsMeasuring = true;dispatchLayoutStep2();// now we can get the width and height from the children.mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);}
}
可以看到这里最终还是调用了defaultOnMeasure()这个方法在之前进行了介绍,但是与之前不同的一点是在此之后又调用了dispatchLayoutStep1()和dispatchLayoutStep2(),并且如果需要二次测量的话会重新执行
dispatchLayoutStep2()。
那么这两个方法具体起到了什么作用呢?
先来看一下这两个方法的源码:
private void dispatchLayoutStep1() {... ...processAdapterUpdatesAndSetAnimationFlags();... ...if (mState.mRunSimpleAnimations) {// 找到没有被remove的ItemView,保存OldViewHolder信息,准备预布局}if (mState.mRunPredictiveAnimations) {// 进行预布局} else {clearOldPositions();}onExitLayoutOrScroll();stopInterceptRequestLayout(false);mState.mLayoutStep = State.STEP_LAYOUT;
}private void processAdapterUpdatesAndSetAnimationFlags() {if (mDataSetHasChangedAfterLayout) {// 因为数据集意外更改,处理这些项目没有价值。 // 相反,我们只是重置它。mAdapterHelper.reset();if (mDispatchItemsChangedEvent) {mLayout.onItemsChanged(this);}}// 简单动画是高级动画的一个子集(这将导致预布局步骤)如果布局支持预测动画,// 则进行预处理以决定我们是否要运行它们if (predictiveItemAnimationsEnabled()) {mAdapterHelper.preProcess();} else {mAdapterHelper.consumeUpdatesInOnePass();}boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;//重点,这里主要设置了mRunSimpleAnimations和mRunPredictiveAnimations的值//mFirstLayoutComplete是指第一次绘制流程完成,当未完成时为false//因此当一次绘制的时候mRunSimpleAnimations和mRunPredictiveAnimations都为false//不会加载动画mState.mRunSimpleAnimations = mFirstLayoutComplete&& mItemAnimator != null&& (mDataSetHasChangedAfterLayout|| animationTypeSupported|| mLayout.mRequestedSimpleAnimations)&& (!mDataSetHasChangedAfterLayout|| mAdapter.hasStableIds());mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations&& animationTypeSupported&& !mDataSetHasChangedAfterLayout&& predictiveItemAnimationsEnabled();
}private void dispatchLayoutStep2() {...mState.mItemCount = mAdapter.getItemCount();mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;// Step 2: Run layout//重点,尝试执行layoutChildrenmState.mInPreLayout = false;mLayout.onLayoutChildren(mRecycler, mState);...mState.mLayoutStep = State.STEP_ANIMATIONS;...
}
总结一下,step1的功能是与Animations有关,控制是否加载动画,而step2的功能是尝试layoutChildren。
if (mHasFixedSize) {mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {eatRequestLayout();processAdapterUpdatesAndSetAnimationFlags();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);
}if (mAdapter != null) {mState.mItemCount = mAdapter.getItemCount();
} else {mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear
如果mHasFixedSize为true,就直接调用LayoutManager.onMeasure方法进行测量,如果mHasFixedSize为false,则先判断是否有数据更新,有的话先处理数据更新,再调用LayoutManager.onMeasure方法进行测量。
在onMeasure方法完成之后,接下来应该进行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的时候完成,//但是当size发生变化的时候,仍然需要重新onLayoutif (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 {// always make sure we sync them (to ensure mode is exact)mLayout.setExactMeasureSpecsFrom(this);}//布局的最后一步,我们保存有关动画视图的信息,触发动画并进行任何必要的清理。//这里设置了mState.mLayoutStep = State.STEP_STARTdispatchLayoutStep3();}
public void draw(Canvas c) {super.draw(c);final int count = mItemDecorations.size();//调用了mItemDecorations的onDraw方法//此时item已经在绘制了,这意味着ItemDecoration会在item上方被绘制for (int i = 0; i < count; i++) {mItemDecorations.get(i).onDrawOver(c, this, mState);}... ...
}
@Override
public void onDraw(Canvas c) {super.onDraw(c);//调用了mItemDecorations的onDraw方法//此时item还没有绘制,这意味着ItemDecoration会在item下方被绘制final int count = mItemDecorations.size();for (int i = 0; i < count; i++) {mItemDecorations.get(i).onDraw(c, this, mState);}
}
这里有两个知识要点:
总而言之draw方法主要做了以下几件事情:
在之前的dispatchLayoutStep2中,RecyclerView调用了mLayout.onLayoutChildren,LayoutManager是具体的item展示者,由它来确定item应该如何展示,如何布局。具体的应用可以查看这篇文章:LayoutManager及其自定义。
这里只讲述LayoutManager是如何绘制item的,以LinearLayoutManager为例:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {//布局算法:// 1) 通过检查children和其他变量,找到锚点坐标和锚点项位置。// 2) 向开始填充,从底部堆叠// 3) 向末端填充,从顶部堆叠// 4) 滚动以满足从底部堆叠的要求。// 创建布局状态// ······// 第一步final View focused = getFocusedChild();if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION|| mPendingSavedState != null) {mAnchorInfo.reset();mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;// 计算锚点的位置和坐标updateAnchorInfoForLayout(recycler, state, mAnchorInfo);mAnchorInfo.mValid = true;}// ······// 移除回收子viewdetachAndScrapAttachedViews(recycler);mLayoutState.mIsPreLayout = state.isPreLayout();// 开始填充if (mAnchorInfo.mLayoutFromEnd) {// 向开始填充,更新LayoutStateupdateLayoutStateToFillStart(mAnchorInfo);mLayoutState.mExtra = extraForStart;fill(recycler, mLayoutState, state, false);startOffset = mLayoutState.mOffset;final int firstElement = mLayoutState.mCurrentPosition;if (mLayoutState.mAvailable > 0) {extraForEnd += mLayoutState.mAvailable;}// 向末端填充updateLayoutStateToFillEnd(mAnchorInfo);mLayoutState.mExtra = extraForEnd;mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;fill(recycler, mLayoutState, state, false);endOffset = mLayoutState.mOffset;if (mLayoutState.mAvailable > 0) {// end could not consume all. add more items towards startextraForStart = mLayoutState.mAvailable;updateLayoutStateToFillStart(firstElement, startOffset);mLayoutState.mExtra = extraForStart;fill(recycler, mLayoutState, state, false);startOffset = mLayoutState.mOffset;}} 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 endupdateLayoutStateToFillEnd(lastElement, endOffset);mLayoutState.mExtra = extraForEnd;fill(recycler, mLayoutState, state, false);endOffset = mLayoutState.mOffset;}}// ······
}
可以看到在这个方法中,首先计算出了锚点的位置和坐标,然后以锚点开始向start方向或者end方向进行填充,如果还有剩余位置的话就从另一个方向开始进行填充。
简单总结一下整个流程就是:
fill方法中,真正填充的方法是layoutChunk
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {// ······while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {// ······layoutChunk(recycler, state, layoutState, layoutChunkResult);}// ······
}
layoutChunk的执行流程如下:
调用LayoutState的next方法获得一个ItemView。千万别小看这个next方法,RecyclerView缓存机制的起点就是从这个方法开始,可想而知,这个方法到底为我们做了多少事情。
如果RecyclerView是第一次布局Children的话(layoutState.mScrapList == null为true),会先调用addView,将View添加到RecyclerView里面去。
调用measureChildWithMargins方法,测量每个ItemView的宽高。注意这个方法测量ItemView的宽高考虑到了两个因素:1.margin属性;2.ItemDecoration的offset。
调用layoutDecoratedWithMargins方法,布局ItemView。这里也考虑上面的两个因素的。
在讲述缓存之前,不妨先思考一下,为什么RecyclerView需要缓存机制?
在RecyclerView的Adapter中,对于不同的数据类型会为其创建一个ViewHolder,并且将数据绑定到这个ViewHolder上。
public class MyAdapter extends RecyclerView.Adapter {@Overridepublic MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){... ...}@Overridepublic void onBindViewHolder(@NonNull MyViewHolder holder, int position){... ...}
}
但是无论是onCreateViewHolder还是onBindViewHolder都会使用到findViewById方法,如果每次一个新item进入都需要回调这两个方法会导致效率很低,因此需要对ViewHolder进行缓存复用以减少回调频次。
先看看这篇文章吧
这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!
public final class Recycler {.../*** Attempts to get the ViewHolder for the given position, either from the Recycler scrap,* cache, the RecycledViewPool, or creating it directly.* * 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。* ...*/@NullableViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {if (mState.isPreLayout()) {// 0 尝试从mChangedScrap中获取ViewHolder对象holder = getChangedScrapViewForPosition(position);...}if (holder == null) {// 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);...}if (holder == null) {...final int type = mAdapter.getItemViewType(offsetPosition);if (mAdapter.hasStableIds()) {// 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);...}if (holder == null && mViewCacheExtension != null) {// 2 尝试从mViewCacheExtension中获取ViewHolder对象final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);if (view != null) {holder = getChildViewHolder(view);...}}if (holder == null) { // fallback to pool// 3 尝试从mRecycledViewPool中获取ViewHolder对象holder = getRecycledViewPool().getRecycledView(type);...}if (holder == null) {// 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图holder = mAdapter.createViewHolder(RecyclerView.this, type);...}}if (mState.isPreLayout() && holder.isBound()) {...} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {...// 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}...return holder;}...
}
从上述代码,可以看到RecyclerView在尝试获取ViewHolder的时候会依次从mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecycledViewPool获取,如果都获取不到才会创建viewHolder。
mChangedScrap、mAttachedScrap主要用于存放当前屏幕可见,但是被标记为“移除”或者“重用”的列表项。
mChangedScrap主要用于notifyItemChanged、notifyItemRangeChanged这类方法在动画开启下的场景,而mAttachedScrap则用于notifyItemMoved、notifyItemRemoved这类列表项发生移动的场景。
当调用notifyItemRemoved后,RecyclerView首先会将所有的可见的viewHolder加入到mAttachedScrap中,等到重新布局完成,开始展示子视图之后再遍历mAttachedScrap找到对应position的viewHolder。
mCachedViews用于存放已经被移除屏幕,但是很快有可能进入屏幕的列表项,默认大小为2。这里比较好理解,假设RecyclerView向下滑动,当最上面的viewHolder滚动出可见区的时候,这个viewHolder会加入到mCachedViews中。之后如果反方向滑动,当最上面的viewHolder再次进入到可见区域之后,就会尝试从mCachedViews再次获取之前的viewHolder。
mViewCacheExtension主要用于提供额外的、开发人员使用的缓冲区。
mRecyclerPool主要用于按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项。
public static class RecycledViewPool {private static final int DEFAULT_MAX_SCRAP = 5;//自定义了数据类型ScrapData,每个ScrapData内部存有最多5个同一viewType类型的viewHolder。static class ScrapData {final ArrayList mScrapHeap = new ArrayList<>();int mMaxScrap = DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs = 0;long mBindRunningAverageNs = 0;}SparseArray mScrap = new SparseArray<>();...
}
先看看这篇文章吧
掌握这17张图,没人比你更懂RecyclerView的预加载
RecyclerView 的滚动是怎么实现的?(一)
RecyclerView 的滚动时怎么实现的?(二)| Fling
简单来说,RecyclerView回去捕获MotionEvent.ACTION_MOVE事件,并且计算出移动的距离。
当移动超过一定距离则进入Fling状态,每一帧都会去执行移动任务。
当计算出移动距离之后,委托LayoutManager执行当前的item向反方向移动的动画,并且填充空闲区域。