RecycleView源码浅析之Recycler+滑动
来源:互联网 发布:ping 域名 ip不一样 编辑:程序博客网 时间:2024/06/05 00:17
概述
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.javapublic void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, @Nullable ItemHolderInfo postInfo) { mRecycler.unscrapView(viewHolder); animateDisappearance(viewHolder, info, postInfo); }//RV.javavoid 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.javaprivate 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的文章。
- RecycleView源码浅析之Recycler+滑动
- Recycleview之setAdapter源码分析
- ListView之Recycler机制
- android之RecycleView之ItemTouchHelper 处理拖拽、滑动删除
- scrollview嵌套recycleview滑动冲突的问题之2
- RECYCLER
- Recycler
- 横向滑动的Recycleview
- RecycleView 禁止滑动
- RecycleView 滑动优化
- recycleView保持流畅滑动
- 解决RecycleView嵌套RecycleView滑动冲突问题
- Netty源码 Recycler 对象池全面解析
- recycleView滑动删除,拖动排序
- RecycleView横向滑动item变大
- recycleView滑动删除,拖动排序
- Android 禁止RecycleView的滑动
- viewPager + recycleView 实现左右滑动
- [POJ1083]Moving Tables
- 时间序列数据的首选神经网络
- 系统广播的收到短信和来电/去电
- C++ STL使用,以及注意事项
- MYSQL的REPLACE和ON DUPLICATE KEY UPDATE使用
- RecycleView源码浅析之Recycler+滑动
- Linux下的tar压缩解压缩命令详解
- 使用exportfs命令来重新加载/etc/exports文件
- 【Android Studio】Live Template
- Oracle like 优化
- AS template
- Android百度地图定位sdk 类方法参数、定位原理详细介绍
- cordova camera插件——摄像头插件的使用及上传图片
- 八爪鱼采集:赶集网电话号码采集图文教程