关于在项目中需不需要将ListView替换成RecyclerView,这个问题前段时间碰到过,今天就来分析下RecyclerView的实现机制。
1.ListView的实现机制
这块我就不多说,网上关于这块的剖析还是很多的,还是直接贴出郭霖大神的那篇博文吧Android ListView工作原理完全解析,带你从源码的角度彻底理解,如果对于ListView的工作原理感兴趣的,可以搭乘时光机去瞅瞅,这里就不多介绍。
2.RecyclerView工作原理
关于RecyclerView,官方的说法它是ListView的增强版本,它的新特性如ViewHolder、ItemDecorator、LayoutManager、SmothScroller以及增加或删除item时item动画等,而且可以对于listView和GridView进行替代。当在处理复杂布局的情况下,利用RecyclerView会比ListView更加的方便跟灵活。 接下来,我们来分析RecycleView大体的实现机制。在Android中,一个View的绘制大体会经过Measure、Layout、Draw这几个方法,RecyclerView当然也会经历这几个过程。我们首先看看RecyclerView的onMeasure()方法:
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(...);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
...
}
我们截取onMeasure()方法中重要的代码进行分析,不难发现,RecyclerView的绘制过程其实是交给了mLayout这个对象,这个对象其实就是我们LayoutManager的实例对象。我们看看主要代码
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
上面onMeasure()方法其实就是做了初始化RecyclerView的属性操作;接着我们看看dispatchLayoutStep1():
private void dispatchLayoutStep1() {
...
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 = ...
...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(...);
mViewInfoStore.addToPreLayout(holder, animationInfo);
...
}
}
...
}
截取了部分代码内容,这个方法做的操作其实就是Pre-Layout,我们关注三个地方,第一,ItemHolderInfo:这个对象保存了RecycleView中的一些边界信息。
public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,
@AdapterChanges int flags) {
final View view = holder.itemView;
this.left = view.getLeft();
this.top = view.getTop();
this.right = view.getRight();
this.bottom = view.getBottom();
return this;
}
第二,mViewInfoStore,这个对象保存的是RecyclerView中item的动画信息。
/**
* Keeps data about views to be used for animations
*/
final ViewInfoStore mViewInfoStore = new ViewInfoStore();
第三,addToPreLayout(),这个方法主要就是将pre-layout阶段ItemView的信息保存在mViewInfoStore中的mLayoutHolderMap中,做的是一些保存信息操作。
/**
* Adds the item information to the prelayout tracking
* @param holder The ViewHolder whose information is being saved
* @param info The information to save
*/
void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.preInfo = info;
record.flags |= FLAG_PRE;
}
接着,回到onMeasure()方法,继续跟进dispatchLayoutStep2()。首先还是看看源码:
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
...
mLayout.onLayoutChildren(mRecycler, mState);
...
}
从源码的注释我们能够看出,这一步的操作就是布局子控件。 mLayout.onLayoutChildren(mRecycler, mState);
进入该方法我们发现,这个方法是个空方法,必须要被重写。也就是说,在实现RecyclerView.LayoutManager这个抽象类的时候,需要重写onLayoutChildren()这个方法,由于在RecyclerView中,提供了3个RecyclerView.LayoutManager的实现类,我们就以LinearLayoutManager类中的onLayoutChildren()方法来进行说明(similar functionality to {@link android.widget.ListView})。
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
...
ensureLayoutState();
...
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
...
}
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
...
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
...
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
...
} else {
// fill towards end
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;
}
// fill towards start
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 end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...
}
我们可以从注释中了解到,onLayoutChidren()这个方法主要是通过获取一个anchor (锚点)的信息来以此为起始方向或者结束方向来填充itemView。在上述方法中,真正执行填充的方法不难发现是fill()这个方法。我们跟进fill()这个方法:
/**
The magic functions :). Fills the given layout, defined by the layoutState. This is fairly independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager} and with little change, can be made publicly available as a helper class.
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
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;
}
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;
}
在上述方法中,有一个while()循环,其中有一个layoutChunk()内部方法,我们继续看一下:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
...
LayoutParams params = (LayoutParams) view.getLayoutParams();
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);
}
}
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
...
// 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);
...
}
在这个方法中,所做的事情就是添加子view(addView())与测量子view的大小(measureChildWithMargins())。我们看看最后调用的一个方法,layoutDecoratedWithMargins():
/**
Lay out the given child view within the RecyclerView using coordinates that include any current {@link ItemDecoration ItemDecorations} and margins.
*/
public void layoutDecoratedWithMargins(View child, int left, int top, int right,int bottom) {
...
child.layout(...);
}
这个方法就是布局子view的方法。接下来我们梳理下RecyclerView填充ItemView的算法:1.向父容器中添加子控件;2.测量子控件的大小;3.将测量后的子控件进行布局,将锚点向布局的方向进行平移,直到RecyclerView的子控件全部填充或者其绘制空间已绘制完毕。RecyclerView的绘制空间就是RecyclerView的父容器规定RecyclerView所占的空间大小。
接下来就是来看看RecyclerView的draw()方法部分,同所有的view的绘制一样,RecyclerView在这个方法将子控件一一的绘制出来。
@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);
}
...
}
@Override
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);
}
}
我们发现,draw()方法十分的简单,就是确定子View距离边界的offset,然后将其画出来。
RecyclerView的滑动机制
了解RecyclerView的滑动机制首先我们来看看其onTouchEvent()这个方法:
@Override
public boolean onTouchEvent(MotionEvent e) {
...
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);
...
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis);
} break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
...
case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
...
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
...
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} break;
...
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally ?
-VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
final float yvel = canScrollVertically ?
-VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
...
}
...
}
RecyclerView的滑动机制主要的思想为:首先接触到ACTION_MOVE事件之后,会计算出手指在屏幕上的移动距离(dx、dy),然后根据该移动的距离与mTouchSlop(阈值)进行对比,从而设置RecyclerView的滑动状态(setScrollState(SCROLL_STATE_DRAGGING);),最终,通过调用scrollByInternal()这个方法实现滑动。这个时候的滑动并没有结束,继续接受ACTION_UP事件之后,RecyclerView会通过VelocityTracker来计算其滑动的一个初始速度(xvel、yvel)当拿到这个速度值之后,调用fling()方法实现后续的滑动,最后初始化RecyclerView的touch事件。总结下说,就是RecyclerView的滑动主要涉及到了两个核心的方法,scrollByInternal()与fling()。
1.scrollByInternal()
看看源码:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
...
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
...
}
从上述方法中,我们可以看到该方法兜兜转转,最后还是会调用到LinearLayoutManager中的ScrollBy()方法。这里就不在赘述。
2.fling()
老样子,还是先看源码:
public boolean fling(int velocityX, int velocityY) {
...
if (canScroll) {
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
fling()方法中核心的代码就是mViewFlinger.fling(velocityX, velocityY);这句。mViewFlinger是一个runnable对象,而RecyclerView中的fling()还是通过Scroller来实现的。继续深入:
public ViewFlinger() {
mScroller = ScrollerCompat.create(getContext(), sQuinticInterpolator);
}
//mViewFlinger.fling(...)
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
继续看看mViewFlinger中的run()方法,看看里面到底做了什么事情:
public void run() {
...
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
final int dx = x - mLastFlingX;
final int dy = y - mLastFlingY;
...
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (dx != 0) {
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
...
if (!awakenScrollBars()) {
invalidate();
}
...
if (scroller.isFinished() || !fullyConsumedAny) {
setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
} else {
postOnAnimation();
}
}
...
}
这段代码中需要注意一个方法mLayout.scrollHorizontallyBy(dx, mRecycler, mState);这个方法最终会走到LinearLayoutManager中的ScrollBy()方法。上段代码的思想就是根据Scroller事件偏移量来计算itemView的平移距离或者是在此创建itemView。
最后我们来看看LinearLayoutManager中的ScrollBy()方法:
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
....
}
在这个方法中,我们可以看到RecyclerView填充itemView的绘制区域就是滑动事情的偏移量,而 mOrientationHelper.offsetChildren(-scrolled);这个方法就是平移itemView的方法。所以RecyclerView的滑动最终都是通过ScrollBy()这个方法进行操作的。
3.RecyclerView的复用机制
首先,我们回顾一下在前面的onLayoutChildren()这个方法中,使用的 fill(recycler, mLayoutState, state, false)方法来填充itemView,跟进之后我们发现,其实最终是调用RecyclerView.Recycler.getViewForPosition()方法。
View getViewForPosition(int position, boolean dryRun) {
...
boolean fromScrap = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder =getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
...
if (holder == null) {
...
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(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 recycler
// try recycler.
// Head to the shared pool.
...
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
mAdapter.bindViewHolder(holder, offsetPosition);
...
}
...
}
getViewForPosition()这个方法比较长,但是不难理解,如果您对listView的复用机制有过了解,那么这里理解起来就相对的容易一些。梳理下这个方法的思路:这个方法是根据列表位置获取ItemView,先后从scrapped、cached、exCached、recycled集合中查找相对应的ItemView,最后没有找到的话,就新建一个新的holder( holder = mAdapter.createViewHolder(RecyclerView.this, type))最后与数据集进行绑定显示。scrapped、cached、exCached集合是定义在RecyclerView.Recycler中,scrapped表示废弃的ItemView,这里可以理解为删除掉的ItemView,cached与exCached分别表示一级和二级缓存的ItemView。recycled集合就是一个map,它是定义在RecyclerView.RecyclerViewPool中,它的作用就是将ItemView以ItemType分类保存起来。
好了,讲述了RecyclerView复用的ItmeView是从哪里来的,那么什么时候它们被添加进了复用的集合中呢?scrapped集合中的ItemView是当我们执行remove操作释放的ItemView,而caced集合中的ItemView呢?答案当然是在前面布局的时候,还记得在布局onLayoutChildren()方法中的核心方法fill()时,当我们循环的向RecyclerView的可绘制区域添加ItmeView的时候,其实最后执行了 recycleByLayoutState(recycler, layoutState)这个方法,这个方法我们最终可以追溯到RccyclerView.Recyler.recycleViewHolderInternal()方法:
/**
* internal implementation checks if view is scrapped or attached and throws an exception
*/
void recycleViewHolderInternal(ViewHolder holder) {
...
if (forceRecycle || holder.isRecyclable()) {
if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize --;
}
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
}
...
}
从源码中我们可以发现,其实在RecyclerView的layout阶段就将ItmeView存入了cached集合,上述代码的逻辑思路为,判断cached集合是否存满,若存满了,则将ItmeView移除到recycled集合中,若没有存满,则添加ItemView到cached集合中。
RecyclerView动画机制
RecyclerView的动画其实也是跟数据集与ItemView绑定在一起的,在RecyclerView中定义了4种对数据集的操作,分别为Add、Remove、Update、Move。这4个数据集的操作封装在AdapterHelper.UpdateOp类中,而且所有的操作由一个大小为30的对象池进行管理,当我们执行任何一个操纵的时候,都会从对象池中取出一个UpdateOp对象,最后调用RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法,这个方法会跟据等待队列中的信息,对所有的子控件进行重新测量、布局、重绘且执行动画。
我们举个栗子,当我们删除某个ItemView的时候,我们会调用Adapter.notifyItemRemove()这个方法。最终会追溯到RecyclerView.RecyclerViewDataObserver.onItemRangeRemove():
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
...
if(mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
通过上面源码可以发现,在执行onItemRangeRemoved()这个方法时,会调用triggerUpdateProcessor()这个方法,我们再进入triggerUpdateProcessor()这个方法看看:
void triggerUpdateProcessor() {
if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
在这个方法里面会调用requestLayout()这个方法,那么接下来的操作就跟RecyclerView的绘制流程一致了,还有个问题,动画是在什么时候执行的?
回想下我们在布局的时候,关注过dispatchLayoutStep2这个方法,其实动画的执行是在dispatchLayoutStep3中执行的。看源码:
/**
* The final step of the layout where we save the information about views for animations,
* trigger animations and do any necessary cleanup.
*/
private void dispatchLayoutStep3() {
...
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
...
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
...
}
我们关注下step4,这里注释上明确说明触发动画的地方。我们看看process()这个方法的源码:
void process(ProcessCallback callback) {
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index --) {
final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
final InfoRecord record = mLayoutHolderMap.removeAt(index);
...
}
}
我们注意下传入的ProcessCallback 这个对象:
/**
* The callback to convert view info diffs into animations.
*/
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);
}
...
};
注意animateDisappearance(),这里就是我们执行动画的方法所在,我们再跟进查看下:
private void animateDisappearance(@NonNull ViewHolder holder,
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
**addAnimatingView(holder);**
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
*postAnimationRunner();*
}
}
首先,说明下addAnimatingView()这个方法,这个方法最终会向mHiddenViews这个集合中添加ItemView,而所添加的ItemView,最终会以getViewForPosition()方法中的getScrapViewForPosition()来获取。接着我们说下animateDisappearance()这个方法,它其实是将动画与ItmeView进行绑定,并且添加到一个等待的执行的队列中,而当postAnimationRunner()这个方法被调用的时候,就会执行这个队列中的动画,这样就实现了RecyclerView的操作数据集的动画效果。