RecyclerView剖析

article/2025/11/10 16:48:30

####简介
  本文将从RecyclerView实现原理并结合源码详细分析这个强大的控件。阅读本文要求:1、熟悉android控件绘制,2、了解动画,3、了解Scroller.本文所示源码版本是23.2.0。

####基本使用
  RecyclerView的基本使用并不复杂,只需要提供一个RecyclerView.Apdater的实现用于处理数据集与ItemView的绑定关系,和一个RecyclerView.LayoutManager的实现用于 测量并布局 ItemView。

####绘制流程
  众所周知,android控件的绘制可以分为3个步骤:measure、layout、draw。RecyclerView的绘制自然也经这3个步骤。但是,RecyclerView将它的measure与layout过程委托给了RecyclerView.LayoutManager来处理,并且,它对子控件的measure及layout过程是逐个处理的,也就是说,执行完成一个子控件的measure及layout过程再去执行下一个。下面看下这段代码:

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;}...dispatchLayoutStep2();mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);...} else {...}
}

这是RecyclerView的测量方法,再看下dispatchLayoutStep2()方法:

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

上面的mLayout就是一个RecyclerView.LayoutManager实例。通过以上代码(和方法名称),不难推断出,RecyclerView的measure及layout过程委托给了RecyclerView.LayoutManager。接着看onLayoutChildren方法,在兼容包中提供了3个RecyclerView.LayoutManager的实现,这里我就只以LinearLayoutManager来举例说明:

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....mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;// calculate anchor position and coordinateupdateAnchorInfoForLayout(recycler, state, mAnchorInfo);...if (mAnchorInfo.mLayoutFromEnd) {...} else {// fill towards endupdateLayoutStateToFillEnd(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 startupdateLayoutStateToFillStart(mAnchorInfo);mLayoutState.mExtra = extraForStart;mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;fill(recycler, mLayoutState, state, false);startOffset = mLayoutState.mOffset;...}...
}

源码中的注释部分我并没有略去,它已经解释了此处的逻辑了。这里我以垂直布局来说明,mAnchorInfo为布局锚点信息,包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)——这里是指start、end方向。这部分代码的功能就是:确定布局锚点,以此为起点向开始和结束方向填充ItemView,如图所示:
在这里插入图片描述

在上一段代码中,fill()方法的作用就是填充ItemView,而图(3)说明了,在上段代码中fill()方法调用2次的原因。虽然图(3)是更为普遍的情况,而且在实现填充ItemView算法时,也是按图(3)所示来实现的,但是mAnchorInfo在赋值过程(updateAnchorInfoForLayout)中,只会出现图(1)、图(2)所示情况。现在来看下fill()方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {...int remainingSpace = layoutState.mAvailable + layoutState.mExtra;LayoutChunkResult layoutChunkResult = new LayoutChunkResult();while (...&&layoutState.hasMore(state)) {...layoutChunk(recycler, state, layoutState, layoutChunkResult);...if (...) {layoutState.mAvailable -= layoutChunkResult.mConsumed;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);}}...
}

下面是layoutChunk()方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {View view = layoutState.next(recycler);...if (layoutState.mScrapList == null) {if (mShouldReverseLayout == (layoutState.mLayoutDirection== LayoutState.LAYOUT_START)) {addView(view);} else {addView(view, 0);}}...measureChildWithMargins(view, 0, 0);...// 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);...
}

这里的addView()方法,其实就是ViewGroup的addView()方法;measureChildWithMargins()方法看名字就知道是用于测量子控件大小的,这里我先跳过这个方法的解释,放在后面来做,目前就简单地理解为测量子控件大小就好了。下面是layoutDecoreated()方法:

public void layoutDecorated(...) {...child.layout(...);
}

总结上面代码,在RecyclerView的measure及layout阶段,填充ItemView的算法为:向父容器增加子控件,测量子控件大小,布局子控件,布局锚点向当前布局方向平移子控件大小,重复上诉步骤至RecyclerView可绘制空间消耗完毕或子控件已全部填充。
  这样所有的子控件的measure及layout过程就完成了。回到RecyclerView的onMeasure方法,执行 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec)这行代码的作用就是根据子控件的大小,设置RecyclerView的大小。至此,RecyclerView的measure和layout实际上已经完成了。
  但是,你有可能已经发现上面过程中的问题了:如何确定RecyclerView的可绘制空间?不过,如果你熟悉android控件的绘制机制的话,这就不是问题。其实,这里的可绘制空间,可以简单地理解为父容器的大小;更准确的描述是,父容器对RecyclerView的布局大小的要求,可以通过MeasureSpec.getSize()方法获得——这里不包括滑动情况,滑动情况会在后文描述。需要特别说明的是在23.2.0版本之前,RecyclerView是不支持WRAP_CONTENT的。先看下RecyclerView的onLayout()方法:

protected void onLayout(boolean changed, int l, int t, int r, int b) {...dispatchLayout();...
}

这是dispatchLayout()方法:

void dispatchLayout() {...if (mState.mLayoutStep == State.STEP_START) {dispatchLayoutStep1();...dispatchLayoutStep2();}dispatchLayoutStep3();...
}

可以看出,这里也会执行子控件的measure及layout过程。结合onMeasure方法对skipMeasure的判断可以看出,如果要支持WRAP_CONTENT,那么子控件的measure及layout就会提前在RecyclerView的测量方法中执行完成,也就是说,先确定了子控件的大小及位置后,再由此设置RecyclerView的大小;如果是其它情况(测量模式为EXACTLY),子控件的measure及layout过程就会延迟至RecyclerView的layout过程(RecyclerView.onLayout())中执行。再看onMeasure方法中的mLayout.mAutoMeasure,它表示,RecyclerView的measure及layout过程是否要委托给RecyclerView.LayoutManager,在兼容包中提供的3种RecyclerView.LayoutManager的这个属性默认都是为true的。好了,以上就是RecyclerView的measure及layout过程,下面来看下它的draw过程。
  RecyclerView的draw过程可以分为2部分来看:RecyclerView负责绘制所有decoration;ItemView的绘制由ViewGroup处理,这里的绘制是android常规绘制逻辑,本文就不再阐述了。下面来看看RecyclerView的draw()和onDraw()方法:

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

可以看出对于decoration的绘制代码上十分简单。但是这里,我必须要抱怨一下RecyclerView.ItemDecoration的设计,它实在是太过于灵活了,虽然理论上我们可以使用它在RecyclerView内的任何地方绘制你想要的任何东西——到这一步,RecyclerView的大小位置已经确定的哦。但是过于灵活,太难使用,以至往往使我们无从下手。
  好了,题外话就不多说了,来看看decoration的绘制吧。还记得上面提到过的measureChildWithMargins()方法吗?先来看看它:

 public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);widthUsed += insets.left + insets.right;heightUsed += insets.top + insets.bottom;final int widthSpec = ...final int heightSpec = ...if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {child.measure(widthSpec, heightSpec);}}

这里是getItemDecorInsetsForChild()方法:

 Rect getItemDecorInsetsForChild(View child) {...final Rect insets = lp.mDecorInsets;insets.set(0, 0, 0, 0);final int decorCount = mItemDecorations.size();for (int i = 0; i < decorCount; i++) {mTempRect.set(0, 0, 0, 0);mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);insets.left += mTempRect.left;insets.top += mTempRect.top;insets.right += mTempRect.right;insets.bottom += mTempRect.bottom;}lp.mInsetsDirty = false;return insets;
}

方法getItemOffsets()就是我们在实现一个RecyclerView.ItemDecoration时可以重写的方法,通过mTempRect的大小,可以为每个ItemView设置位置偏移量,这个偏移量最终会参与计算ItemView的大小,也就是说ItemView的大小是包含这个位置偏移量的。我们在重写getItemOffsets()时,可以指定任意数值的偏移量:
在这里插入图片描述

4个方向的位置偏移量对应mTempRect的4个属性(left,top,right,bottom),我以top offset的值在垂直线性布局中的应用来举例说明下。如果top offset等于0,那么ItemView之间就没有空隙;如果top offset大于0,那么ItemView之前就会有一个间隙;如果top offset小于0,那么ItemView之间就会有重叠的区域。
  当然,我们在实现RecyclerView.ItemDecoration时,并不一定要重写getItemOffsets(),同样的对于RecyclerView.ItemDecoration.onDraw()或RecyclerView.ItemDecoration.onDrawOver()方法也不是一定要重写,而且,这个绘制方法和我们所设置的位置偏移量没有任何联系。下面我来实现一个RecyclerView.ItemDecoration来加深下这里的理解:我将在垂直线性布局下,在ItemView间绘制一条5个像素宽、只有ItemView一半长、与ItemView居中对齐的红色分割线,这条分割线在ItemView内部top位置。

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {Paint paint = new Paint();paint.setColor(Color.RED);for (int i = 0; i < parent.getLayoutManager().getChildCount(); i++) {final View child = parent.getChildAt(i);float left = child.getLeft() + (child.getRight() - child.getLeft()) / 4;float top = child.getTop();float right = left + (child.getRight() - child.getLeft()) / 2;float bottom = top + 5;c.drawRect(left,top,right,bottom,paint);}
}@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {outRect.set(0, 0, 0, 0);
}

代码不是很严谨,大家姑且一看吧,当然这里getItemOffsets()方法可以省略的。
  以上就是RecyclerView的整个绘制流程了,值得注意的地方也就是在23.2.0中RecyclerView支持WRAP_CONTENT属性了;还有就是ItemView的填充算法fill()算是一个亮点吧。接下来,我将分析ReyclerView的滑动流程。

####滑动
  RecyclerView的滑动过程可以分为2个阶段:手指在屏幕上移动,使RecyclerView滑动的过程,可以称为scroll;手指离开屏幕,RecyclerView继续滑动一段距离的过程,可以称为fling。现在先看看RecyclerView的触屏事件处理onTouchEvent()方法:

public boolean onTouchEvent(MotionEvent e) {...if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}...switch (action) {...case MotionEvent.ACTION_MOVE: {...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) {...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: {...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事件后,会先计算出手指移动距离(dy),并与滑动阀值(mTouchSlop)比较,当大于此阀值时将滑动状态设置为SCROLL_STATE_DRAGGING,而后调用scrollByInternal()方法,使RecyclerView滑动,这样RecyclerView的滑动的第一阶段scroll就完成了;当接收到ACTION_UP事件时,会根据之前的滑动距离与时间计算出一个初速度yvel,这步计算是由VelocityTracker实现的,然后再以此初速度,调用方法fling(),完成RecyclerView滑动的第二阶段fling。显然滑动过程中关键的方法就2个:scrollByInternal()与fling()。接下来同样以垂直线性布局来说明。先来说明scrollByInternal(),跟踪进入后,会发现它最终会调用到LinearLayoutManager.scrollBy()方法,这个过程很简单,我就不列出源码了,但是分析到这里先暂停下,去看看fling()方法:

public boolean fling(int velocityX, int velocityY) {...mViewFlinger.fling(velocityX, velocityY);...
}

有用的就这一行,其它乱七八糟的不看也罢。mViewFlinger是一个Runnable的实现ViewFlinger的对象,就是它来控件着ReyclerView的fling过程的算法的。下面来看下类ViewFlinger的一段代码:

void postOnAnimation() {if (mEatRunOnAnimationRequest) {mReSchedulePostAnimationCallback = true;} else {removeCallbacks(this);ViewCompat.postOnAnimation(RecyclerView.this, this);}
}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();
}

可以看到,其实RecyclerView的fling是借助Scroller实现的;然后postOnAnimation()方法的作用就是在将来的某个时刻会执行我们给定的一个Runnable对象,在这里就是这个mViewFlinger对象,这部分原理我就不再深入分析了,它已经不属于本文的范围了。并且,关于Scroller的作用及原理,本文也不会作过多解释。对于这两点各位可以自行查阅,有很多文章对于作过详细阐述的。接下来看看ViewFlinger.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) {...if (dy != 0) {vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);overscrollY = dy - vresult;}...}...if (!awakenScrollBars()) {invalidate();//刷新界面}...if (scroller.isFinished() || !fullyConsumedAny) {setScrollState(SCROLL_STATE_IDLE);} else {postOnAnimation();}}...
}

本段代码中有个方法mLayout.scrollVerticallyBy(),跟踪进入你会发现它最终也会走到LinearLayoutManager.scrollBy(),这样虽说RecyclerView的滑动可以分为两阶段,但是它们的实现最终其实是一样的。这里我先解释下上段代码。第一,dy表示滑动偏移量,它是由Scroller根据时间偏移量(Scroller.fling()开始时间到当前时刻)计算出的,当然如果是RecyclerView的scroll阶段,这个偏移量也就是手指滑动距离。第二,上段代码会多次执行,至到Scroller判断滑动结束或已经滑动到边界。再多说一下,postOnAnimation()保证了RecyclerView的滑动是流畅,这里涉及到著名的“android 16ms”机制,简单来说理想状态下,上段代码会以16毫秒一次的速度执行,这样其实,Scroller每次计算的滑动偏移量是很小的一部分,而RecyclerView就会根据这个偏移量,确定是平移ItemView,还是除了平移还需要再创建新ItemView。
在这里插入图片描述

现在就来看看LinearLayoutManager.scrollBy()方法:

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {...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);...
}

如上文所讲到的fill()方法,作用就是向可绘制区间填充ItemView,那么在这里,可绘制区间就是滑动偏移量!再看方法 mOrientationHelper.offsetChildren()作用就是平移ItemView。好了整个滑动过程就分析完成了,当然RecyclerView的滑动还有个特性叫平滑滑动(smooth scroll),其实它的实现就是一个fling滑动,所以就不再赘述了。

####Recycler
  Recycler的作用就是重用ItemView。在填充ItemView的时候,ItemView是从它获取的;滑出屏幕的ItemView是由它回收的。对于不同状态的ItemView存储在了不同的集合中,比如有scrapped、cached、exCached、recycled,当然这些集合并不是都定义在同一个类里。
  回到之前的layoutChunk方法中,有行代码layoutState.next(recycler),它的作用自然就是获取ItemView,我们进入这个方法查看,最终它会调用到RecyclerView.Recycler.getViewForPosition()方法:

View getViewForPosition(int position, boolean dryRun) {...// 0) If there is a changed scrap, try to find from thereif (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);fromScrap = holder != null;}// 1) Find from scrap by positionif (holder == null) {holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);...}if (holder == null) {...// 2) Find from scrap via stable ids, if existsif (mAdapter.hasStableIds()) {holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);...}if (holder == null && mViewCacheExtension != null) {final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);if (view != null) {holder = getChildViewHolder(view);...}}if (holder == null) {...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);...}...
}

这个方法比较长,我先解释下它的逻辑吧。根据列表位置获取ItemView,先后从scrapped、cached、exCached、recycled集合中查找相应的ItemView,如果没有找到,就创建(Adapter.createViewHolder()),最后与数据集绑定。其中scrapped、cached和exCached集合定义在RecyclerView.Recycler中,分别表示将要在RecyclerView中删除的ItemView、一级缓存ItemView和二级缓存ItemView,cached集合的大小默认为2,exCached是需要我们通过RecyclerView.ViewCacheExtension自己实现的,默认没有;recycled集合其实是一个Map,定义在RecyclerView.RecycledViewPool中,将ItemView以ItemType分类保存了下来,这里算是RecyclerView设计上的亮点,通过RecyclerView.RecycledViewPool可以实现在不同的RecyclerView之间共享ItemView,只要为这些不同RecyclerView设置同一个RecyclerView.RecycledViewPool就可以了。
  上面解释了ItemView从不同集合中获取的方式,那么RecyclerView又是在什么时候向这些集合中添加ItemView的呢?下面我逐个介绍下。
  scrapped集合中存储的其实是正在执行REMOVE操作的ItemView,这部分会在后文进一步描述。
  在fill()方法的循环体中有行代码recycleByLayoutState(recycler, layoutState);,最终这个方法会执行到RecyclerView.Recycler.recycleViewHolderInternal()方法:

void recycleViewHolderInternal(ViewHolder holder) {...if (forceRecycle || holder.isRecyclable()) {if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE)) {// Retire oldest cached viewfinal int cachedViewSize = mCachedViews.size();if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0);}if (cachedViewSize < mViewCacheMax) {mCachedViews.add(holder);cached = true;}}if (!cached) {addViewHolderToRecycledViewPool(holder);recycled = true;}}...}

这个方法的逻辑是这样的:首先判断集合cached是否満了,如果已満就从cached集合中移出一个到recycled集合中去,再把新的ItemView添加到cached集合;如果不満就将ItemView直接添加到cached集合。
  最后exCached集合是我们自己创建的,所以添加删除元素也要我们自己实现。

####数据集、动画
  RecyclerView定义了4种针对数据集的操作,分别是ADD、REMOVE、UPDATE、MOVE,封装在了AdapterHelper.UpdateOp类中,并且所有操作由一个大小为30的对象池管理着。当我们要对数据集作任何操作时,都会从这个对象池中取出一个UpdateOp对象,放入一个等待队列中,最后调用RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法,根据这个等待队列中的信息,对所有子控件重新测量、布局并绘制且执行动画。以上就是我们调用Adapter.notifyItemXXX()系列方法后发生的事。
  显然当我们对某个ItemView做操作时,它很有可以会影响到其它ItemView。下面我以REMOVE为例来梳理下这个流程。
在这里插入图片描述

首先调用Adapter.notifyItemRemove(),追溯到方法RecyclerView.RecyclerViewDataObserver.onItemRangeRemoved():

public void onItemRangeRemoved(int positionStart, int itemCount) {assertNotInLayoutOrScroll(null);if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {triggerUpdateProcessor();}
}

这里的mAdapterHelper.onItemRangeRemoved()就是向之前提及的等待队列添加一个类型为REMOVE的UpdateOp对象, triggerUpdateProcessor()方法就是调用View.requestLayout()方法,这会导致界面重新布局,也就是说方法RecyclerView.onLayout()会随后调用,这之后的流程就和在绘制流程一节中所描述的一致了。但是动画在哪是执行的呢?查看之前所列出的onLayout()方法发现dispatchLayoutStepX方法共有3个,前文只解释了dispatchLayoutStep2()的作用,这里就其它2个方法作进一步说明。不过dispatchLayoutStep1()没有过多要说明的东西,它的作用只是初始化数据,需要详细说明的是dispatchLayoutStep3()方法:

private void dispatchLayoutStep3() {...if (mState.mRunSimpleAnimations) {// Step 3: Find out where things are now, and process change animations....// Step 4: Process view info lists and trigger animationsmViewInfoStore.process(mViewInfoProcessCallback);}...
}

代码注释已经说明得很清楚了,这里我没有列出step 3相关的代码是因为这部分只是初始化或赋值一些执行动画需要的中间数据,process()方法最终会执行到RecyclerView.animateDisappearance()方法:

private void animateDisappearance(...) {addAnimatingView(holder);holder.setIsRecyclable(false);if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {postAnimationRunner();}
}

这里的animateDisappearance()会把一个动画与ItemView绑定,并添加到待执行队列中, postAnimationRunner()调用后就会执行这个队列中的动画,注意方法addAnimatingView():

private void addAnimatingView(ViewHolder viewHolder) {final View view = viewHolder.itemView;...mChildHelper.addView(view, true);...
}

这里最终会向ChildHelper中的一个名为mHiddenViews的集合添加给定的ItemView,那么这个mHiddenViews又是什么东西?上节中的getViewForPosition()方法中有个getScrapViewForPosition(),作用是从scrapped集合中获取ItemView:

ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {...View view = mChildHelper.findHiddenNonRemovedView(position, type);...
}

接下来是findHiddenNonRemovedView()方法:

View findHiddenNonRemovedView(int position, int type) {final int count = mHiddenViews.size();for (int i = 0; i < count; i++) {final View view = mHiddenViews.get(i);RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()&& (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {return view;}}return null;
}

Oops!看到这里就我之前所讲的scrapped集合联系起来了,虽然绕了个圈。所以这里就论证我之前对于scrapped集合的理解。
  文章到这里也快结束了,最后关于动画,本节提到的对数据集的4种操作,在DefalutItemAnimator中给出了对应的默认实现,就是改变透明度,实现淡入淡出效果。如果要自定义ItemView的动画可以参考这里的实现来做。好了,以上就是我对于RecyclerView的全部剖析了,也许还有我没有提及的方面,或是我讲错的地方,欢迎指正。

####结束语
  之所以写这篇文章,是因为之前一直没有找到关于RecyclerView实现原理上分析的文章,找到的都是怎么使用的,所有写下本文,希望能对此感兴趣的同学有些许帮助。

Written with StackEdit.


http://chatgpt.dhexx.cn/article/Xf80IlVV.shtml

相关文章

RecyclerView的使用(一)

目录 1.RecyclerView概述 2.RecyclerView的简单使用 3.改变布局管理器&#xff0c;RecyclerView的变化 1.RecyclerView概述 在谷歌Android官网&#xff0c;给RecyclerView的描述是: 那RecyclerView凭什么要比ListView要更高级&#xff1f;更灵活&#xff1f; 答案是&#x…

RecyclerView详解

RecyclerView 简称 RV&#xff0c; 是作为 ListView 和 GridView 的加强版出现的&#xff0c;目的是在有限的屏幕之上展示大量的内容&#xff0c;因此 RecyclerView 的复用机制的实现是它的一个核心部分。 RV 常规使用方式如下&#xff1a; 解释说明。 setLayoutManager&…

RecyclerView(三)—— RecyclerView的缓存机制

RecyclerView内存优越性&#xff0c;得益于它独特的缓存机制。 1 如何复用表项 如果列表中的每个表项在移出屏幕时被销毁&#xff0c;移入时又被重新创建&#xff0c;是很消耗资源&#xff0c;所以RecyclerView引入了缓存机制。缓存是为了复用&#xff0c;复用的好处是有可能…

RecyclerView详解一,使用及缓存机制

本文大致会先讲解RecyclerView的基础知识及使用&#xff0c;最后会深入讲解一点原理。当然&#xff0c;本人知识水平有限哈&#xff0c;太深入的东西我现在还没接触到&#xff0c;还请大家包容&#xff0c;阿里嘎多~ 一、RecyclerView的历史与发展 既然讲到了RV&#xff0c;那…

Android开发—RecyclerView使用

1.RecyclerView是什么 RecyclerView 在Android中用于创建列表。 官网的解释为&#xff1a; RecyclerView 可以让您轻松高效地显示大量数据。您提供数据并定义每个列表项的外观&#xff0c;而 RecyclerView 库会根据需要动态创建元素。 当RecyclerView的列表项滚出屏幕的时候&a…

Android RecyclerView使用简述

RecyclerView使用简述 前言正文一、创建项目二、RecyclerView基本使用① item布局和适配器② 显示数据③ 添加Item点击事件④ 添加Item子控件点击事件⑤ 添加长按事件⑥ 多个子控件点击事件 三、RecyclerView ViewBinding使用① 适配器② 显示数据③ 添加控件点击和长按 四、R…

es6新特性总结及使用说明

目录 简介 新特性说明 let语法 const语法 解构赋值 模板字符串 对象简写 对象操作--深拷贝 箭头函数 小结 简介 1. ECMAScript 6.0是 JavaScript 语言的下一代标准&#xff0c; 2015 年 6 月发布。 ES6 设计目标是达到 JavaScript 语言可以用来编写复杂的大型程序&a…

ES6有哪些新特性

ES6有哪些新特性(1)变量声明:由var变为let和const; (2)模板字符串:使用反引号;在模板字符串里面支持换行,并可以在里面使用${}来包裹一个变量或表达式; (3)解构:有数组解构和对象解构;可以快速获取数组和对象的值; (4) 展开运算符:在ES6中用…来表示展开运算符,它可以将数组…

ES6 新特性知识点总结

文章目录 ES6let及const解构赋值模板字符串Symbol类型Set和Map数据结构箭头函数类 ES6 ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。 ES6 实际上是一个泛指&#xff0c;泛指 ES2015 及后续的版本。 每一次标准的诞生都意味着语言的…

ES6 新特性

1 、ES6 新特性 现在使用主流的前端框架中&#xff0c;如ReactJS、Vue.js、angularjs等&#xff0c;都会使用到ES6的新特性&#xff0c;作为一名高级工程师而言&#xff0c;ES6也就成为了必修课&#xff0c;所以本套课程先以ES6的新特性开始。 1.1、了解ES6 ES6&#xff0c;…

ES6新特性总结-面试必会

文章目录 一、let和const二、Symbol三、模板字符串3.1 什么是模板字符串3.2 字符串新方法 四、解构表达式4.1 数组解构4.2 对象解构 五、Set()、map()数据结构5.1 map()是什么及写法&#xff1f;5.1.1 map()是什么及写法&#xff1f;5.1.2 map()下的内置方法&#xff1a; 5.2 S…

JavaScript ES6新特性

JavaScript ES6带来了新的语法和特性&#xff0c;使得代码更加的现代和可读。它包括许多重要功能&#xff0c;如箭头函数、模板字符串、解构赋值等等。 const 和 let const 是 ES6 中用于声明变量的新关键字。const 比 var 强大。一旦使用&#xff0c;变量就不能重新分配。换…

ES6必须知道的六大新特性

ES6 ES6新特性-let&const 使用const表示常量&#xff08;声明之后不允许改变&#xff0c;一旦声明必须初始化&#xff0c;否则会报错&#xff09; //ES6 常量 不能修改const b2;b3;//Uncaught TypeError: Assignment to constant variable.console.log(b);使用var声明的…

ES6中有哪些新特性?

ES6中的新特性(一) ECMAScript6.0&#xff08;以下简称 ES6&#xff09;是 JavaScript 语言的下一代标准&#xff0c;已经在 2015 年 6 月正式发布了。它的目标&#xff0c;是使得 JavaScript 语言可以用来编写复杂的大型应用程序&#xff0c;成为企业级开发语言。 我们来看看…

面试题!es6新特性

es6新特性 ECMAScript 6(ES6) 目前基本成为业界标准&#xff0c;它的普及速度比 ES5 要快很多&#xff0c;主要原因是现代浏览器对 ES6的支持相当迅速&#xff0c;尤其是 Chrome 和 Firefox 浏览器&#xff0c;已经支持 ES6 中绝大多数的特性。 以下是一些常用到的es6新特性&…

es6的8条新特性总结

es6的8条新特性总结 认识es61.块级作用域变量&#xff08;let和const&#xff09;2.箭头函数3.模板字符串4.解构赋值5.默认参数6. 扩展运算符7. 类和继承8.Promise 认识es6 ES6&#xff08;ECMAScript 2015&#xff09;是JavaScript的新版本&#xff0c;引入了许多新特性和语法…

最全的—— ES6有哪些新特性?

目录 ES6新特性1、let和const2、symbol3、模板字符串3.1 字符串新方法&#xff08;补充&#xff09; 4、解构表达式4.1 数组解构4.2 对象解构 5、对象方面5.1 Map和Set5.1.1 Map5.1.2 Set 5.3 数组的新方法5.3.1 Array.from()方法5.3.2 includes()方法5.3.3 map()、filter() 方…

GWAS分析中协变量的区分(性别?PCA?初生重?)

1. 电子书领取 前几天发了一篇GWAS电子书分享&#xff0c;异常火爆&#xff0c;阅读量8000&#xff0c;很多人评价比较基础。这本电子书主要特点是比较基础&#xff0c;GLM模型用软件和R语言进行比较&#xff0c;如何添加数字协变量、因子协变量、PCA等内容&#xff0c;可以说…

时间序列 工具库学习(5) Darts模块-多个时间序列、预训练模型和协变量的概念和使用

1.实验目的 此笔记本用作以下目的&#xff1a; 在多个时间序列上训练单个模型使用预训练模型获取训练期间未见的任何时间序列的预测使用协变量训练和使用模型 2.导库 # fix python path if working locally from utils import fix_pythonpath_if_working_locallyfix_python…

Mplus数据分析:随机截距交叉之后的做法和如何加协变量,写给粉丝

记得之前有写过如何用R做随机截距交叉滞后&#xff0c;有些粉丝完全是R小白&#xff0c;还是希望我用mplus做&#xff0c;今天就给大家写写如何用mplus做随机截距交叉滞后。 做之前我们需要知道一些Mplus的默认的设定&#xff1a; observed and latent exogenous variables a…