RecyclerView是Android 5.0开始提供一个可回收容器,它比 ListView更先进灵活更具扩展性,可高效重用和滚动,能方便定制各种布局和交互效果。它继承自 ViewGroup,那么它展示和交互也离不开 measure , layout , draw , touch ,adapter 五步。后面将以这五步来抽丝剥茧分析其工作原理,其中 LayoutManager 以最简单的 LinearLayoutManager为例。更多经典自定义 ViewGroup集合
RecyclerView 的辅助类非常多,相联紧密的类都作为其内部类存在,如下图:
在这个类之外还有比较重要的三个类:AdapterHelper,ChildHelper,ViewInfoStore 。AdapterHelper负责处理 Adapter 里的数据集发生变化时的预处理;ChildHelper负责管理和访问 RecyclerView 的子视图;ViewInfoStore 记录 pre-layout 和 post-layout 阶段的 ViewHolder 状态信息,方便分析差异做相应的动画。
首先需要引入 recyclerview-v7 的包,在 build.gradle的 dependencies 块中添加
compile 'com.android.support:recyclerview-v7:25.2.0@aar'
或
compile(name: 'recyclerview-v7-25.2.0', ext: 'aar') //直接引用 aar 文件,需要flatDir中添加 aar 目录dirs。
使用时自已的 YourAdapter 需要继承自 RecyclerView.Adapter,YourViewHolder 要继承RecyclerView.ViewHolder.
除此对 RecyclerView 还需要设置 LayoutManager,SDK提供了常用的三种布局管理器LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager。要设置divider或其它修饰可通过RecyclerView的addItemDecoration来定制。而事件可以在 Adapter 的onBindViewHolder里去绑定。总的来说 RecyclerView 的定制性是相当高的,用法就不多讲,下面重点讲解其工作原理。
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;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
......
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
......
}else{
......
}
首先调用了LayoutManager的onMeasure,内部只是调用了RecyclerView的defaultOnMeasure,这一步实质是在忽略子视图情况下测绘容器大小。当RecyclerView本身的宽和高都是精确值即没有wrap_content
时skipMeasure为true 此时子视图的测绘将会延迟到 onLayout 过程,后续会讲到。skipMeasure 为 true 或 adapter 为空时都是没有必要测绘子视图来决定自身大小的。不确定自身大小(存在wrap_content
)才会走后续的 dispatchLayoutStep1()和 dispatchLayoutStep2()方法。而测绘和布局子视图就是在dispatchLayoutStep2
这一步完成的。当调用mLayout.setMeasuredDimensionFromChildren
,内部会调用 RecyclerView.setMeasuredDimension方法从而定格了 RecyclerView 自身容器的大小。
注:dispatchLayoutStep2在onLayout 中也有调用稍后会讲到其具体实现。
mState.mLayoutStep
的值调用不同的dispatchLayout步骤,源码概要如下:void dispatchLayout() {
......
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
可以发现 layout 过程也像 onMeasure 里有类似调用 即dispatchLayoutStep1和dispatchLayoutStep2. 这是因为RecyclerView对子视图的 measure 和 layout 都是通过dispatchLayoutStep2这个方法一起完成的。当然我们不必担心它会重复调用了,因为还有一个mState.mLayoutStep
来控制测绘步骤呢。当值为STEP_START
时会调用dispatchLayoutStep1之后赋值mLayoutStep = State.STEP_LAYOUT
,当值为STEP_LAYOUT
时才会调用dispatchLayoutStep2然后再赋值mLayoutStep = State.STEP_ANIMATIONS
,而当值为STEP_ANIMATIONS
时才会调用dispatchLayoutStep3然后重新赋值为STEP_START
。确保 onMeasure 到 onLayout 中各步骤只会有一次。
每一次数据集变化或是调用了 requestLayout 都会走dispatchLayoutStep这几个方法。这个方法也是 RecyclerView 对子视图填充布局和做动画的核心所在。上面在 onMeasure 和 onLayout 时也多次出现,以下就讲综合讲述它们各阶段的作用 。
(回顾 measure
和回顾 layout
)
其中最重要的是实现了子视图测量和填充的方法是dispatchLayoutStep2,它是,下面我们先讲
dispatchLayoutStep1和dispatchLayoutStep3,这两个方法都是与执行不同操作的动画紧密相关且相对简单些(动作如 Adapter 的 add、remove、insert 等)。
private void dispatchLayoutStep1() {
......
mViewInfoStore.clear();
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
......
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
......
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
if (mState.mRunPredictiveAnimations) {
......
}
......
mState.mLayoutStep = State.STEP_LAYOUT;
}
这部分主要是对已经存在容器里的 View 的 ViewHolder预处理信息分类暂时存放到mViewInfoStore
中,为后面做动画有个差异分析提供基础。
private void dispatchLayoutStep3() {
......
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
......
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
}
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
mViewInfoStore.process(mViewInfoProcessCallback);
}
.....
}
经过dispatchLayoutStep2重新填充后视图会更新或多或少,通过对比 dispatchLayoutStep1 阶段暂存在mViewInfoStore中的 pre-layout 信息,做一些处理后将当前ViewHolder和动画信息添加到mViewInfoStore的 post-layout 中。这两个阶段 addToPreLayout 和 addToPostLayout 都会添加相应的标记,最后调用mViewInfoStore.process(mViewInfoProcessCallback)
在内部进行处理后并回调才执行相应操作的动画。见下面动画预处理后的回调处源码:
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
new ViewInfoStore.ProcessCallback() {
@Override
public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
@Nullable ItemHolderInfo postInfo) {
mRecycler.unscrapView(viewHolder);
animateDisappearance(viewHolder, info, postInfo);
}
@Override
public void processAppeared(ViewHolder viewHolder,
ItemHolderInfo preInfo, ItemHolderInfo info) {
animateAppearance(viewHolder, preInfo, info);
}
......
}
RecyclerView 的子视图测量和布局,最终还是委托给了 LayoutManager 来处理见源码:
private void dispatchLayoutStep2() {
......
mLayout.onLayoutChildren(mRecycler, mState);
......
}
//LinearLayoutManager 的onLayoutChildren方法
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
......
updateAnchorInfoForLayoutExpose(state, mAnchorInfo);
......
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start 和下面的逻辑分支是反过来的,仅看下面的逻辑为讲解。
......
} else {
// fill towards end
updateLayoutStateToFillEndExpose(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStartExpose(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
......
}
detachAndScrapAttachedViews
,回收过程讲解;以描点为界向两个方向填充的算法示意图如下,其中红色点为描点位置:
初始化时无描点的 itemView 此时按 (1)或(2)进行,当长视图滑动时填充就会出现(3)示意的填充。LinearLayoutManager单一方向的填充算法源码如下:
protected int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
......
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
......
recycleByLayoutStateExpose(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
while (remainingSpace > 0 && layoutState.hasMore(state)) {
layoutChunkResultCache.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResultCache);
if (layoutChunkResultCache.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResultCache.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResultCache.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResultCache.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResultCache.mConsumed;
} ......
}
if (DEBUG) {
validateChildOrderExpose();
}
return start - layoutState.mAvailable;
}
首先会通过recycleByLayoutStateExpose
方法移除已经滑出屏幕以外的子视图,然后通过一个循环的朝着一个布局方向每次调用layoutChunk
添加 View,并适时调节剩余空间和下次 layout 的 offset,直到剩下的空间不足循环结束。而子视图逐个的测量和布局就是在layoutChunk这个方法里进行的,看下面部分源码:
protected void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
......
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
// can not find in scrapList
if (mShouldReverseLayoutExpose == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
......
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (getOrientation() == 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 {
......
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
......
result.mFocusable = view.isFocusable();
}
此方法通过layoutState.next(recycler)复用机制获取下一个需要布局的 View, 将它通过 ViewGroup 的 addView 方法添加到 RecyclerView 里面来,然后通过measureChildWithMargins(view, 0, 0)
测量它的大小,最后通过layoutDecorated
方法实际上调用了 child.layout(left,top,right,bottom)来把子视图布局到相应的位置。
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
final ArrayList mCachedViews = new ArrayList();
private final List
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
......
}
先讲View和ViewHolder 的创建。接上文,layoutChunk 添加View时调用了layoutState.next(recycler)来获取下个 View,实际调用了 Recycler的getViewForPosition方法。
public View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextFromLimitedList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
//以下 next 会调用下面方法。
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
最终是调用Recycler的tryGetViewHolderForPositionByDeadline 这个方法分以下几步:
(1)检查mChangedScrap,若匹配到则返回相应holder
(2)检查mAttachedScrap,若匹配到且holder有效则返回相应holder
(3)检查mViewCacheExtension,若匹配到则返回相应holder
(4)检查mRecyclerPool,若匹配到则返回相应holder
(5)否则执行Adapter.createViewHolder(),新建holder实例
(6)返回holder再通过 holder.itemView 得到了要添加的 View.
此方法比较长,看下面关键源码:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
......
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
......
}
if (holder == null) {
......
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
......
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
......
}
}
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
......
}
if (holder == null) {
......
holder = mAdapter.createViewHolder(RecyclerView.this, type);
......
}
}
......
boolean bound = false;
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
......
return holder;
}
基本上就是通过各级缓存获取 ViewHolder,获取不到就调用mAdapter.createViewHolder 创建 。最终的 ViewHolder 如果需要绑定就会调用tryBindViewHolderByDeadline来绑定数据,其内部调用了mAdapter.bindViewHolder。
在重新设置 Adapter 或通知某个item 变法时,相应的 ViewHolder 会加上标志位如UPDATE,REMOVE 等,待onLayoutChildren时就会先调用detachAndScrapAttachedViews其最终也是调用了Recycler 的的recycleViewHolderInternal方法。
滑动过程中不断回收不可见的 ViewHolder,见fill 方法中的recycleByLayoutStateExpose,最终是调用了LayoutManager 的 removeAndRecycleViewAt。
//先移除再回收,
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
而 removeViewAt 是通过帮助类ChildHelper来移除的,最终回收仍是调用了Recycler 的recycleViewHolderInternal
回收关键方法recycleViewHolderInternal很长,关键源码如下:
void recycleViewHolderInternal(ViewHolder holder) {
......
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
......
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
......
mViewInfoStore.removeViewHolder(holder);
}
对于移除的 View 如果是有效的无update 或remove 动作就添加到mCachedViews中,否则失效的 ViewHolder 会添加到RecycledViewPool中。
在 Recycler的各级缓存中,只有RecycledViewPool是按照 viewType 去存储和获取缓存的 ViewHolder 来达到复用,其它缓存需要匹配 layoutPosition 或 getItemId再加上 viewType 。缓存级别最高的是mChangedScrap它是预处理layout时添加的,对应方法 dispatchLayoutStep1;mAttachedScrap是其次,它是attach到容器未移除的缓存复用很适用频繁抖动滑动;mCachedViews再次,它是 detach 的 ViewHolder 缓存,mViewCacheExtension是用户扩展的缓存池一般用户是没有实现的,最后才是 RecyclerViewPool它只需要按viewType 返回。
public void addItemDecoration(ItemDecoration decor) {
addItemDecoration(decor, -1);
}
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
......
}
public static abstract class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state){ }
public void onDrawOver(Canvas c, RecyclerView parent, State state){}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {}
}
在对 itemView 画装饰时候,为了实现 Item 之间的间隔或是避免画的内容覆盖到 itemView 上,就需要通过getItemOffsets返回,itemView 的需要预留出的四边内间距,如下视图。
整个 item 所占的空间也就是最外面虚线的边界,在 measure 过程中这部分其实也是考虑进去了。
fling平滑滑动过程是借助ViewFlinger不断postOnAnimation 内部调用ScrollerCompat工具进行计算,每次滑动 dx,dy,这就和ACTION_MOVE 时的滑动一致了。
最终手势滑动或 fling 滑动都会回调给 LayoutManager的scrollHorizontallyBy(dx…)或scrollVerticallyBy(dy…)内部又会调用 scrollBy 如下
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
......
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
......
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
可见滑动过程也会调用 LinearLayoutManager 的 fill 方法,上面讲过,内部会回收移除屏幕的 View和 ViewHolder,并填充新的View 和 ViewHolder.
每次重新设置不同的 Adapter 或是更改 Adapter 里的数据集合并调用 notifyItemXXX 时,最终都会触发当前存在的 ViewHolder里的标记位发生变化。加上FLAG_UPDATE, FLAG_REMOVE等不同操作的标记位。然后再 requestLayout 后会重新走 上面讲到的 dispatchLayoutStep 的三个流程。界面的变化更新也主要是在第dispatchLayoutStep2 后完成。
Adapter 改变后比较简单,直接重置所有无效。清除所有的缓存 ViewHolder 调用了removeAndRecycleViews();
并且调用markKnownViewsInvalid
标记了容器中的所有 View 无效(FLAG_UPDATE)。
在设置 Adapter 给 RecyclerView 时就会绑定一个数据变化时的观察者DataObserver。当更改数据集后用户需要调用
notifyItemChanged,notifyItemRemoved,notifyItemInsert 等方法,然后DataObserver就会接收到相应的 onItemChaged,onItemRemoved,onItemInsert 等回调并由AdapterHelper对应的onItemXXX来接管。在AdapterHelper内每个操作都对应一个UpdateOp
static class UpdateOp {
static final int ADD = 1;
static final int REMOVE = 1 << 1;
static final int UPDATE = 1 << 2;
static final int MOVE = 1 << 3;
static final int POOL_SIZE = 30;
int cmd;
int positionStart;
......
}
AdapterHelper的onItemXXX 方法是有有bool型的返回值,为true表示需要处理触发triggerUpdateProcessor
调用,实际post 执行以下 Runnable:
final Runnable mUpdateChildViewsRunnable = new Runnable() {
@Override
public void run() {
if (!mFirstLayoutComplete || isLayoutRequested()) {
// a layout request will happen, we should not do layout here.
return;
}
if (!mIsAttached) {
requestLayout();
// if we are not attached yet, mark us as requiring layout and skip
return;
}
if (mLayoutFrozen) {
mLayoutRequestEaten = true;
return; //we'll process updates when ice age ends.
}
consumePendingUpdateOperations();
}
};
没有首次layout 过,或是layout 后续即将触发时直接return ,这里就防止了频繁操作会多次执行操作。consumePendingUpdateOperations
方法,调用AdapterHelper的preProcess
进行预处理内部预处理暂存的一系列 UpdateOp 对象后再通过callback 回调给RecyclerView,而RecyclerView 再传给了 Recycler 处理,即对 ViewHolder 添加不同的标记。
最后调用了dispatchLayout 进行重新执行 dispatchLayoutStep 三步来更新视图。在dispatchLayoutStep1 中也会调用processAdapterUpdatesAndSetAnimationFlags
来处理mAdapterHelper 中的UpdateOp.
至此 RecyclerView 工作的源码分析基本完成,其相关辅助类比较多,源码很长,不对或不详尽之处请大家多提意见,后续再分享 VLayout,欢迎拍砖.更多经典自定义 ViewGroup集合