RecycleView源码浅析之Recycler+滑动
概述
Recycler解决了两个哲学问题,VH从哪里来以及VH到哪里去,前两篇讲到RV的绘制流程和动画都回避了View的获取以及回收问题,其实是因为Recycler帮我们完成了而且封装得很好。这一篇就来看看Recycler是如何帮我们做到这些的,顺带看一下RV这个ViewGroup对触摸事件的处理。
onTouchEvent()
和ViewPager差不多,RV分为拖动和fling,scrollByInternal()产生拖动,fling()产生滑动。
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
...
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
...
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);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} 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;
//fling
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
...
return true;
}
首先看一下scrollByInternal()(看了好多博客貌似都是错的,内部并没有用到scrollBy而是用的layout产生滑动效果),最终调用到scrollBy(),这个方法并不是重写的View#scrollBy(签名不同),而是根据dy重新布局了一次,即用layout产生滑动的效果。那么也就是说滑动的时候VH的回收就是fill()中对VH的回收的逻辑,稍后再说。
boolean scrollByInternal(int x, int y, MotionEvent ev) {
//将更新映射到VH上
consumePendingUpdateOperations();
if (mAdapter != null) {
//y方向上scroll
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
}
...
return consumedX != 0 || consumedY != 0;
}
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
...
return scrollBy(dy, recycler, state);
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
//确定布局方向
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
//dy的绝对值
final int absDy = Math.abs(dy);
//更新LayoutState
updateLayoutState(layoutDirection, absDy, true, state);
//开始布局
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
return scrolled;
}
接下来看看fling()
public boolean fling(int velocityX, int velocityY) {
...
//这里fling
mViewFlinger.fling(velocityX, velocityY);
}
ViewFlinger是RV的一个内部类,实现了Runnable接口,有一个ScrollerCompat成员。其fling()调用了ScrollerCompat#fling(),这个方法和ScrollerCompat#startScroll()类似,初始化了一些值并设置了Scroller的模式,然后需要有不断地回调+计算+改变内容的过程,这是由下面的postOnAnimation()启动的,启动后会执行run()。computeScrollOffset()本质上就是在FLING模式下更新坐标。最终我们又看到了scrollVerticallyBy(),最终会进入LM的scrollBy(),利用fill()进行布局和回收。看来殊途同归。
class ViewFlinger implements Runnable{
...
private ScrollerCompat mScroller;
...
public void fling(int velocityX, int velocityY) {
//设置了模式,还有一些变量,用于fling效果。
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
//把这个Runnable post
postOnAnimation();
}
@Override
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;
int hresult = 0;
int vresult = 0;
mLastFlingX = x;
mLastFlingY = y;
int overscrollX = 0, overscrollY = 0;
if (mAdapter != null) {
//重新布局
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
}
....
if (scroller.isFinished() || !fullyConsumedAny) {
setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
} else {
//没有结束继续调用
postOnAnimation();
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
}
}
}
...
}
}
Recycler的一些概念
上面简单地把拖动和fling的过程过了一遍,最终都是利用fill()进行重新布局达到了滑动的效果。而fill()中不仅从Recycler中获取View,也把超出布局范围的View交给Recycler。
Recycler不仅是VH的回收者,也是View(VH)的提供者。我们先看一些关于Recycler的概念。
-
三级缓存
第一级:mAttachedScrap、mChangedScrap、mCachedViews第二级:ViewCacheExtension(可选,让使用者自己配置)
第三级:RecycledViewPool(RV之间共享VH的缓存池)
-
View的detach和remove
都是针对VG的
被detach的View从VG的View[]数组(保存child)中移除(但在其他地方还有引用),这个轻量级的移除通常用来改变View在数组中的位置。
被remove的View从VG中真正移除
-
Recycler的scrap和recycle
recycle一般配合view的remove,被recycle的VH进入mCachedViews
scrap一般配合View的detach,被scrap的VH进入mAttachedScrap或mChangedScrap
getViewForPosition()
这个Recycler的方法解释了View从哪来的问题,因为View的回收的地方很多,而提供View的地方很固定,就是在fill()方法中,所以我们先来讲这个问题。一切缘起都是因为下面这个方法,它在layout的时候被调用获取下一个应该布局的View,然后添加、测量、布局(这些大家可以看我的第一篇关于RV的文章):
//LayoutState.java
/**
* Gets the view for the next element that we should layout.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
随即会调用到Recycler的一个方法getViewForPosition(),最终会调用到tryGetViewHolderForPositionByDeadline()。这个方法会根据position依次从各级缓存寻找VH或直接新建一个,我们先屏蔽“为什么VH会在这级缓存”这个问题,单单来看获得的逻辑。总体来说是这样的:
1.从mChangedScrapView一级缓存中寻找。
2.从mAttachedScrap一级缓存中寻找。
3.从mHiddenViews中寻找。
4.从mCachedViews中寻找。
5.从mViewCacheExtension中寻找。
6.从mRecyclerPool中寻找。
7.如果都没有找到,调用mAdapter.createViewHolder()新建一个VH。
8.检查VH是否需要重新绑定数据(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()),如果需要,调用mAdapter.bindViewHolder()绑定数据。
9.(VH已经持有itemView了)获取View的LayoutParams,并将其mViewHolder成员指向该VH,mPendingInvalidate成员赋值fromScrapOrHiddenOrCache && bound(如果是从scrap或者hidden或者cache中返回的VH或者是VH重新绑定了,那么需要重绘该VH)。
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
* <p>
*/
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 1.从mChangedScrapView一级缓存中寻找。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
}
// 2.从mAttachedScrap一级缓存中寻找。
//3.从mHiddenViews中寻找。
//为什么会在mHiddenViews中寻找呢?是因为某些被移除但需要执行动画的View是被添加到mHiddenViews中的
//4.从mCachedViews中寻找。
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
...
if (holder == null && mViewCacheExtension != null) {
//5.从mViewCacheExtension中寻找。
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
...
}
if (holder == null) { // fallback to pool
//6.从mRecyclerPool中寻找。
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
//7.如果都没有找到,调用mAdapter.createViewHolder()新建一个VH。
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()) {
//8.检查VH是否需要重新绑定数据(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()),如果需要,调用mAdapter.bindViewHolder()绑定数据。
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
//9.(VH已经持有itemView了)获取View的LayoutParams,并将其mViewHolder成员指向该VH,mPendingInvalidate成员赋值fromScrapOrHiddenOrCache && bound(如果是从scrap或者hidden或者cache中返回的VH或者是VH重新绑定了,那么需要重绘该VH)。
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
VH的回收
如果光看上面的过程逻辑还是比较清晰的,但VH回收的地点却比较散,我们只能从我们已知的地方入手,随着学习的不断深入再慢慢补全。
首先我们已知的一个地方就是在LLM真正开始布局之前,会调用下面这个方法。看注释知道LM会先对已经存在的所有VH做一个scrap或者recycle处理。
//layoutManager.java
/**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
*/
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
//layoutManager.java
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
//从方法名上来看,如果VH是无效的 + 没有被移除的 + mAdapter没有StableId,
//那么我们会remove + recycle这个VH
if (viewHolder.isInvalid() && !viewHolder.isRemoved() &&
!mRecyclerView.mAdapter.hasStableIds()) {
//前面解释概念的时候我们说了,remove是和VG相关,而且是compeletely remove,即和VG断绝一切关系
removeViewAt(index);
//和mmCachedViews有关
recycler.recycleViewHolderInternal(viewHolder);
} else {
//把View从VG中detach
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
下面我们来具体看一下recycleViewHolderInternal()和scrapView(),首先是recycleViewHolderInternal()。重点注释在下面,也就是说这个方法是和mCachedViews配合的,被VG remove掉的View,其VH是一定进入mCachedViews的。
void recycleViewHolderInternal(ViewHolder holder) {
...
if (forceRecycle || holder.isRecyclable()) {
//如果不是INVALID、REMOVED、UPDATE、ADAPTER_POSITION_UNKNOWN这几个状态,开始回收
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
// 如果mCachedViews满了,淘汰一个去mRecyclerPool
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
...
//添加到mCachedViews中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.
//如果一个View正在执行动画却被要求回收,那么回收动作交给ItemAnimatorRestoreListener去做
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
接下来是scrapView(),这是和View的轻量级操作detach结合的。重点注释在下面。
/**
* Mark an attached view as scrap.
*
* <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
* for rebinding and reuse. Requests for a view for a given position may return a
* reused or rebound scrap view instance.</p>
*
* @param view View to scrap
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
//如果这个VH有 REMOVED、INVALID其中的状态 或者 没有更新 或者 是可用的已更新的VH(和动画相关)
//那么添加到mAttachedScrap中
//这里对于第一个条件,我们知道在prelayout的时候被remove的VH还是会layout出来
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {//那么如果 没有REMOVED、INVALID其中的状态 且 更新了 且不能重用更新了的VH
//加入到mChangedScrap中
//何时会产生这种情况呢?也就是我们更改了数据后并调用Adapter.notifyItemChanged方法后
//VH会被标记为UPDATE,在scrap的时候有可能进入这个分支被添加到mChangedScrap中的。
//而且我们和上面获取的过程联系起来,如果一个VH从mChangedScrap获取,那么它就有UPDATE的flag,
//会执行bindViewHolder()方法,且LayoutParams.mPendingInvalidate为真,稍后会执行到它的重绘。
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
然后我们能想起来的一个地方就是在拖动或者fling过程中的fill()方法中,我们会把布局后在屏幕外的VH回收了。
recycleByLayoutState(recycler, layoutState);
经过一系列跟踪,最后会调用到这个方法,View的操作是remove。
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param index Index of child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
继续看recycleView(),其思想就是,如果一个View移出了屏幕,那么它必然是进入mCachedViews这一级缓存的。也就是说和滑动相关的回收是和mCachedViews关联的。
public void recycleView(View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
//如果这个VH有TmpDetached标志,我们将它完全移除
//那么何时VH有TmpDetached标志呢?如果一个View被detach,它会给LayoutParams中的VH设置这个标志位。
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
//如果VH在mChangedScrap或者mAttachedScrap中,我们“unScrap”它
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()){
holder.clearReturnedFromScrapFlag();
}
//进行回收保存进mCachedViews,上面已经介绍过
recycleViewHolderInternal(holder);
}
还有一个地方就是在执行消失动画的时候,首先这个VH要unscrap,然后 addAnimatingView()这个方法保证了这个VH的View是作为hidden添加到VG中用于执行动画的。
//ViewInfoStore.ProcessCallback.java
public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
@Nullable ItemHolderInfo postInfo) {
mRecycler.unscrapView(viewHolder);
animateDisappearance(viewHolder, info, postInfo);
}
//RV.java
void animateDisappearance(@NonNull ViewHolder holder,
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
addAnimatingView(holder);
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
//RV.java
private void addAnimatingView(ViewHolder viewHolder) {
final View view = viewHolder.itemView;
final boolean alreadyParented = view.getParent() == this;
mRecycler.unscrapView(getChildViewHolder(view));
//这个View以hidden的身份添加到VG中
if (viewHolder.isTmpDetached()) {
// re-attach
mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
} else if(!alreadyParented) {
mChildHelper.addView(view, true);
} else {
mChildHelper.hide(view);
}
}
最终这里仅仅只是移除了VH并没有进行回收吗?我猜想是有回收的过程,但是没有找到,或者是前面已经对它进行了回收?
//DefaultItemAnimator.java
private void animateRemoveImpl(final ViewHolder holder) {
..
animation.setDuration(getRemoveDuration())
.alpha(0).setListener(new VpaListenerAdapter() {
...
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(holder);
//移除VH
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
总结
这篇文章简单介绍了一下RV的滑动以及View的获取以及回收机制。
滑动依靠的是layout过程改变子View的位置。
Recycler承包了View(VH)的提供以及VH的回收。其中提供的时候会进行分级查找,如果找不到会进行新建,根据具体情况执行绑定数据。VH的回收有很多地方,比较典型的是布局前和滑动时,fill()相关的回收和mCachedViews关联。
由于本人水平有限,对RV的源码跟踪以及理解都还不能达到一个很高的层次,但至少自己心里已经有了一个大致的框架。有错误的地方或不足的地方欢迎大家一起来讨论,我会不断patch这几篇关于RV的文章。
推荐阅读
-
微信小程序之侧边栏滑动实现过程解析(附完整源码)
-
.7-浅析express源码之Router模块(3)-app[METHODS]
-
.35-浅析webpack源码之babel-loader入口文件路径读取
-
.39-浅析webpack源码之parser.parse
-
.38-浅析webpack源码之babel-loader转换js文件
-
.29-浅析webpack源码之Resolver.prototype.resolve
-
Prototype源码浅析 String部分(四)之补充_prototype
-
Prototype源码浅析 String部分(四)之补充_prototype
-
.7-浅析express源码之Router模块(3)-app[METHODS]
-
RecycleView源码浅析之Recycler+滑动