RecycleView继承自ViewGroup,是一种通过在滑动过程中不断回收复用进而实现流畅滑动的控件,RecycleView回收、缓存、复用的对象都是ViewHolder.itemView,也可以说是ViewHolder(后面回详细说明)
// 直接继承关系
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
// some other code
}
用户滑动RecycleView,首先时间会被Recycle.onTouchEvent()
获取,我们直接看MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
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) {
boolean startScroll = false;
if (canScrollHorizontally) {
if (dx > 0) {
dx = Math.max(0, dx - mTouchSlop);
} else {
dx = Math.min(0, dx + mTouchSlop);
}
if (dx != 0) {
startScroll = true;
}
}
if (canScrollVertically) {
if (dy > 0) {
dy = Math.max(0, dy - mTouchSlop);
} else {
dy = Math.min(0, dy + mTouchSlop);
}
if (dy != 0) {
startScroll = true;
}
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
代码前面主要是对滑动参数(水平滑动距离、垂直滑动距离)的合法性进行一些判定,并设定一些状态值(START_SCROLL等),我们直接看最后面的关键函数scrollByInternal()
,最终的滑动操作是由此函数完成;
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
consumePendingUpdateOperations();
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
/**
* some other code
*/
}
这里只展示了scrollByInternal()
部分源码, 可以看到,首先判定了一下mAdapter不为空,这里的mAdapter就是RecycleView.Adapter的一个实例,之后执行scrollStep()
,OK,我们继续来看scrollStep()做了什么:
/**
* Scrolls the RV by 'dx' and 'dy' via calls to
* {@link LayoutManager#scrollHorizontallyBy(int, Recycler, State)} and
* {@link LayoutManager#scrollVerticallyBy(int, Recycler, State)}.
*
* Also sets how much of the scroll was actually consumed in 'consumed' parameter (indexes 0 and
* 1 for the x axis and y axis, respectively).
*
* This method should only be called in the context of an existing scroll operation such that
* any other necessary operations (such as a call to {@link #consumePendingUpdateOperations()})
* is already handled.
*/
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
这里其实注释已经说的很明白了,scrollStep()
会将RecycleView水平滑动dx,垂直滑动dy,并同时会统计实际消耗的滑动事件数。当dx或者dy不为0(即有效滑动)时,会分别通过mLayout.scrollHorizontallyBy()
和mLayout.scrollVerticallyBy()
实现水平和垂直滑动。这里的mLayout,其类型是RecycleView.LayoutManager
,我们继续看mLayout.scrollHorzontallyBy()
:
/**
* {@inheritDoc}
*/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
/**
* {@inheritDoc}
*/
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
可以看到,scrollHorizontallyBy()
和scrollVerticallyBy()
逻辑是一样的,都是通过scrollBy()实现最终的滑动操作
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
mLayoutState.mRecycle = true;
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
我们知道,RecycleView复用的是ViewHolder,用户滑动过程中,旧的(滑到屏幕外的)Item(里面通常包含若干个View,回收的单位是ViewHolder)会被回收,新进入屏幕的会复用之前的View并重新绑定数据,那么我们首先需要关注,滑动过程中,新进入的View到底是如何创建的:
scrap是用来保存被rv移除掉但最近又马上要使用的缓存,比如rv自带item的动画效果,本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画,如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中。除此之外,在一些需要RecycleView不断自动刷新屏幕内数据的场景,典型的比如炒股软件:会不断刷新屏幕内股票价格、涨跌等信息,刷新过程中,View会被缓存到此scrap中。
也是一个非常重要的缓存,就LinearLayoutManager来说,cached缓存默认大小为2,他的容量非常小,所起到的作用就是rv滑动时候刚被移出屏幕的viewholder的收容所,因为rv会认为刚被移出屏幕的viewholder可能马上就会使用到,所以不会立即设置为无效viewholder,会将他们保存到cached中,但又不能将所有移除屏幕的viewholder视为有效viewholder,所以他的默认容量只有两个,可以通过setViewCacheSize(int viewCount)方法修改
第三级缓存,这是一个自定义缓存,rv可以自定义缓存行为,在这里你可以决定缓存的保存逻辑,但是这么个自定义缓存一般都没有见过具体的使用场景,而且自定义缓存需要你对rv中的源码非常熟悉才行,否则在rv执行item动画,或者执行notify的一系列方法后你的自定义缓存是否还能有效就是一个值得考虑的问题,所以一般不太推荐使用该缓存,更多的我觉得这可能是google自已留着方便扩展来使用的,目前来说这还只是个空实现而已,从这点来看其实rv所说的四级缓存本质上还只是三级缓存。
又一个重要的缓存,这也是唯一一个我们开发者可以方便设置的一个(虽然extension也能设置,但是难度大),而且设置方式非常简单,new一个pool传进去就可以了,其他的都不用我们来处理,google已经给我们料理好后事了,这个缓存保存的对象就是那些无效的viewholder,虽然说无效的viewholder上的数据是无效的,但是他的rootview还是可以拿来使用的,这也是为什么最早listview有一个convertView的原因,当然这种机制也被rv很好的继承下来了,pool一般会和cached配合使用,这么来说cached存不下的就会被保存到pool中,毕竟cached的默认容量大小只有2,但是pool容量也是有限的当保存满之后再有viewholder到来的话就只能会无情抛弃掉,它也有一个默认的容量大小
private static final int DEFAULT_MAX_SCRAP = 5;
int mMaxScrap = DEFAULT_MAX_SCRAP;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
ViewPool内部本质上是采用一种稀松数组(SparseArray)存储数据的,这里的mScrap就是缓存池实例,每一个ScrapData对应一种类型(viewType)的缓存数据,针对每一种viewType,默认缓存大小为5,即每一种ViewHolder,缓存至多五个实例;
在layoutChunk()方法中,我们可以看到如下代码:
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
滑动过程中的新加入的View是通过addView()直接加到当前页面View树的,而这个View是通过layoutState.next()方法获取,这里传入的参数类型时RecycleView.Recycle,也就是RecycleView的回收池
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
RecycleView默认会使用key, value方式对每一个view打标签