-
概述
RecyclerView
是大家工作中经常用到的一个组件,它可以设置列表、网格列表、瀑布流网格列表,这三种布局模式都可以设置是横向的还是竖直方向的,而且可以设置是按照从左到右、从上到下的习惯顺序展示呢还是倒序展示。作为ListView和GridView的替代品,我们从源码的角度来分析一下它的原理,从而真正理解它的优势在哪里。 -
设置布局模式
RecyclerView里面有一个mLayout属性,他的类型是RecyclerView.LayoutManager,他负责保存RecyclerView的布局模式,默认是没有赋值的,所以一定需要手动设置,否则哪怕设置了数据也不会显示出任何东西,因为在onMeasure的时候一开始就会判断mLayout是不是null:
@Override protected void onMeasure(int widthSpec, int heightSpec) { if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); return; } ... ... }
如果mLayout为null则会调用defaultOnMeasure方法,这个方法最终只会取minWidth/minHeight和padding的最小值。
这个方法的调用顺序可以不用限制,只要在界面显示出来之前调用都可以,这是因为在setLayoutManager方法中会调用requestLayout方法重新布局。
RecyclerView提供了三种布局模式:LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager,这三个都是继承自RecyclerView.LayoutManager,我们这里大部分流程按照LinearLayoutManager来看,只对另外两种不同的地方加以整理。
-
根据数据显示UI的入口:requestLayout方法
和ListView一样,RecyclerView根据数据填充界面同样是沿着requestLayout逻辑步骤完成的,知道这一点是至关重要的。调用requestLayout的地方有很多,比如setAdapter的时候、setLayoutManager的时候、notifyDataSetChanged的时候等等。
我们知道,调用requestLayout之后会调用onMeasure方法:
@Override protected void onMeasure(int widthSpec, int heightSpec) { if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); return; } if (mLayout.isAutoMeasureEnabled()) { //使用RecyclerView的测量逻辑:dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3 final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); //在这里这个方法没啥用,因为下面会调用RecyclerView的dispatchLayoutStep系列方法重新测量,三大LM都没有实现,因此相当于调用了defaultOnMeasure方法 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; //如果宽高是固定尺寸则不需要根据内容测量 if (measureSpecModeIsExactly || mAdapter == null) { return; } if (mState.mLayoutStep == State.STEP_START) { //决策动画方式、保存当前视图信息等 dispatchLayoutStep1(); } mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; // 真正的测量 dispatchLayoutStep2(); // 根据子View的宽高计算总大小 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); // 如果RecyclerView的宽高不确定且有子View的宽高也没确定则需要再次dispatchLayoutStep2测量 if (mLayout.shouldMeasureTwice()) { mLayout.setMeasureSpecs( MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); mState.mIsMeasuring = true; dispatchLayoutStep2(); mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } } else { ...//通过对应LayoutManager的onMeasure方法实现进行测量 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); ... } }
首先,mLayout.isAutoMeasureEnabled()来判断是否采用RecyclerView自己来测量,默认是false,即通过对应的LayoutManager来测量。
mLayout.onMeasure方法是由LayoutManager中实现的:
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, int heightSpec) { mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); }
也调用了defaultOnMeasure,所以其实就是上面取最小大小。
LinearLayoutManager实现了isAutoMeasureEnabled并返回true,表示交给RecyclerView自行处理测量。
-
自动测量
自动测量分支逻辑中,如果RecyclerView的宽高测量模式不都是EXACTLY的话,说明根据内容来定,这时候会调用一次dispatchLayoutStep2方法先添加View,然后根据子View的总大小作为测量大小,然后这个时候如果判断子View中是否有wrap_content或match_parent的,如果有的话则进行第二次测量,可以看到,以宽度为例,这次测量时mWidth是通过getMeasuredWidth()获取的,即已经变成上次测量后的宽度作为第二次测量的限制宽度了,在layoutChunk方法(layoutChunk方法是添加View时会调用的,后面会说到)中会用到这个限制宽度:
right = getWidth() - getPaddingRight();
第一次的时候使用的是RecyclerView本身在父容器中的限制大小(即RecyclerView的LayoutParams指定的),而第二次使用的是内容布局之后的测量大小,所以这里二测测量的目的就是尝试进一步缩小测量范围(如果有需要的话)。
-
dispatchLayoutStep2方法
上面提到diapaychLayoutStep2是做添加View的工作的,RecyclerView的核心就是这个方法的逻辑。
diapaychLayoutStep2中会调用onLayoutChildren方法,这个方法就是真正的布局View的核心:mLayout.onLayoutChildren(mRecycler, mState)。LayoutManager中的这个方法如下:
public void onLayoutChildren(Recycler recycler, State state) { Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) "); }
可以看到,需要子类重写,所以我们去LinearLayoutManager中去看,代码较长,我们只看主要逻辑即可。
在这个方法中,通过mAnchorInfo.mLayoutFromEnd来判断是从上而下填充还是从下往上填充,这个值来自:
private void resolveShouldLayoutReverse() { // A == B is the same result, but we rather keep it readable if (mOrientation == VERTICAL || !isLayoutRTL()) { mShouldReverseLayout = mReverseLayout; } else { mShouldReverseLayout = !mReverseLayout; } }
mReverseLayout来自:
public void setReverseLayout(boolean reverseLayout) { assertNotInLayoutOrScroll(null); if (reverseLayout == mReverseLayout) { return; } mReverseLayout = reverseLayout; requestLayout(); }
这个方法在构造时调用,结合上面的resolveShouldLayoutReverse方法可以知道,mShouldReverseLayout是由传入的和系统方向同时决定(规则就是负负得正)。
然后会赋值mAchorInfo.mLayoutFromEnd:mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd。
mStackFromEnd是和AbsListView中一样的作用,代表的是整个列表的方向,默认是false,异或操作也表示负负得正,即如果构造时传入了true并且调用了setStackFromEnd(true)方法,则列表会按照正序排列,这里可能是为了兼容老的编码习惯所以保留了mStackFromEnd的逻辑吧。
mOrientationHelper在设置方向的时候生成的:
public void setOrientation(@RecyclerView.Orientation int orientation) { if (orientation != HORIZONTAL && orientation != VERTICAL) { throw new IllegalArgumentException("invalid orientation:" + orientation); } assertNotInLayoutOrScroll(null); if (orientation != mOrientation || mOrientationHelper == null) { mOrientationHelper = OrientationHelper.createOrientationHelper(this, orientation); mAnchorInfo.mOrientationHelper = mOrientationHelper; mOrientation = orientation; requestLayout(); } }
public static OrientationHelper createOrientationHelper( RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) { switch (orientation) { //这里会根据横向列表还是纵向列表生成不同的实现类 case HORIZONTAL: return createHorizontalHelper(layoutManager); case VERTICAL: return createVerticalHelper(layoutManager); } throw new IllegalArgumentException("invalid orientation"); }
以竖直列表为例:
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) { return new OrientationHelper(layoutManager) { @Override public int getEndAfterPadding() { return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom(); } @Override public int getEnd() { return mLayoutManager.getHeight(); } @Override public void offsetChildren(int amount) { mLayoutManager.offsetChildrenVertical(amount); } @Override public int getStartAfterPadding() { return mLayoutManager.getPaddingTop(); } ... ... }
可以看到,这个匿名类对象主要是用来封装调用LayoutManager的一些关于坐标测量方面的工作。
每次重新layout,mAnchorInfo都会记录即将刷新成的列表的第一个可见View项的位置信息。接下来我们来看mAnchorInfo:
static class AnchorInfo { OrientationHelper mOrientationHelper; int mPosition; int mCoordinate; boolean mLayoutFromEnd; boolean mValid; }
通过代码发现,mPosition记录的是列表中显示的第一个(这里的第一个是相对于layout方向来说的,不是特指从上数第一个)View位于集合中的index下标;mCoordinate记录的是第一个可见View在RecyclerView中的起始位置(如果是竖向正序列表的话就是指top),这个可以从以下代码得出:
anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingSavedState.mAnchorOffset;
getEndAfterPadding()得到的是RecyclerView的不含padding(不含padding是因为padding区域不能画子View)的最末端(比如竖向正序列表的bottom),mPendingSavedState.mAnchorOffset是在onSaveInstanceState的时候赋值的:
if (didLayoutFromEnd) { final View refChild = getChildClosestToEnd(); state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(refChild); state.mAnchorPosition = getPosition(refChild); } else { final View refChild = getChildClosestToStart(); state.mAnchorPosition = getPosition(refChild); state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) - mOrientationHelper.getStartAfterPadding(); }
getChildClosestToStart和getChildClosestToEnd方法都是取得当前布局顺序中第一个可见View,这里给它赋的值就是第一个可见View未显示出来部分的长度,例如mOrientationHelper.getDecoratedStart(以mOrientationHelper.为createVerticalHelper方法返回的为例):
@Override public int getDecoratedStart(View view) { final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedTop(view) - params.topMargin; }
public int getDecoratedTop(@NonNull View child) { return child.getTop() - getTopDecorationHeight(child); }
public int getTopDecorationHeight(@NonNull View child) { return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top; }
child.getTop()返回的是当前子View位于父容器中的top,getTopDecorationHeight得到的是这个子View真正的顶端(可能这个顶端此时溢出在父容器之外,即不可见),这两个值相减得到的就是一个偏移量。
综上所述,mCoordinate最终保存的就是这个第一个可见View相对于父容器的初始位置,如果有溢出的话则这个值是负的(正序来说),这里也可以大概能猜出先滚动到指定position再处理偏移的用意。
知道了以上信息,下面的逻辑就好理解了。
final View focused = getFocusedChild(); if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION || mPendingSavedState != null) { mAnchorInfo.reset(); mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate updateAnchorInfoForLayout(recycler, state, mAnchorInfo); mAnchorInfo.mValid = true; } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused) >= mOrientationHelper.getEndAfterPadding() || mOrientationHelper.getDecoratedEnd(focused) <= mOrientationHelper.getStartAfterPadding())) { // This case relates to when the anchor child is the focused view and due to layout // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows // up after tapping an EditText which shrinks RV causing the focused view (The tapped // EditText which is the anchor child) to get kicked out of the screen. Will update the // anchor coordinate in order to make sure that the focused view is laid out. Otherwise, // the available space in layoutState will be calculated as negative preventing the // focused view from being laid out in fill. // Note that we won't update the anchor position between layout passes (refer to // TestResizingRelayoutWithAutoMeasure), which happens if we were to call // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference // child which can change between layout passes). mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); }
第一个分支表示三种情况,一个是初始化的时候,另一个是之前有过滚动的情况,还有就是onRestoreInstanceState恢复的时候(因为mPendingSavedState是这个方法调用时拿到的),这里都会重新设置mAnchorInfo,updateAnchorInfoForLayout如下:
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) { if (updateAnchorFromPendingData(state, anchorInfo)) { if (DEBUG) { Log.d(TAG, "updated anchor info from pending information"); } return; } if (updateAnchorFromChildren(recycler, state, anchorInfo)) { if (DEBUG) { Log.d(TAG, "updated anchor info from existing children"); } return; } if (DEBUG) { Log.d(TAG, "deciding anchor info for fresh state"); } anchorInfo.assignCoordinateFromPadding(); anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; }
首先执行的是updateAnchorFromPendingData方法,这个方法是获取到上次滚动的位置信息或者onRestoreInstanceState中恢复的位置,把信息赋值到mAnchor中,里面用到的mPendingScrollPosition表示的调用scrollToPosition时传入的position。
然后如果之前没有需要恢复的位置,则执行updateAnchorFromChildren方法,这个方法首先会尝试找出拥有焦点的子View并且必须是可见的然后定位到它的位置,若没有则定位到集合中的第一个View,当然这里说的定位指的是锁定第一个要显示的View的位置信息。
最后assignCoordinateFromPadding方法会采用RecyclerView的初始位置作为mCoordinate,表示若是没有有效位置就使用初始位置显示。
在了解了AnchorInfo之后,updateAnchorInfoForLayout方法里面调用的这几个方法的逻辑很好理解,就不贴代码了。
回到onLayoutChildren方法,这里的第二个分支就是处理含有焦点的子View的,逻辑在assignFromViewAndKeepVisibleRect中,updateAnchorFromChildren方法中符合情况时也调用了这个方法来处理焦点View。
这个方法中一开始:
final int spaceChange = mOrientationHelper.getTotalSpaceChange(); if (spaceChange >= 0) { assignFromView(child, position); return; }
这是为了检查是否RecyclerView的内容区域layout方向上的长度发生了变化,如果变化了则使用初始位置。
然后我们取正序的逻辑分支代码来看一下:
final int childStart = mOrientationHelper.getDecoratedStart(child); final int startMargin = childStart - mOrientationHelper.getStartAfterPadding(); mCoordinate = childStart; if (startMargin > 0) { // we have room to fix end as well final int estimatedEnd = childStart + mOrientationHelper.getDecoratedMeasurement(child); final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; final int previousEndMargin = previousLayoutEnd - mOrientationHelper.getDecoratedEnd(child); final int endReference = mOrientationHelper.getEndAfterPadding() - Math.min(0, previousEndMargin); final int endMargin = endReference - estimatedEnd; if (endMargin < 0) { mCoordinate -= Math.min(startMargin, -endMargin); } }
可以看到,mOrientationHelper.getDecoratedStart(child)直接赋给了mCoordinate,为什么这里没有用child.top减去这个值呢,因为焦点View恢复的时候是需要完全展示出来的。最后if代码块中的逻辑主要是处理因为spaceChange变化的偏移(可绘制长度变化了,焦点View的位置也要跟着同步移动)。
回到onLayoutChildren方法,接下来执行代码:
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mLayoutState.mLastScrollDelta记录的是当前布局方向上的位移量,正负代表方向,如果是正数则表示是向下滚动,则这里记录layoutDirection是向下填充。
mReusableIntPair(长度为2的int数组)用来保存上下可滚动的长度,根据mLayoutState.mLayoutDirection来决定,通过calculateExtraLayoutSpace方法设置:
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, @NonNull int[] extraLayoutSpace) { int extraLayoutSpaceStart = 0; int extraLayoutSpaceEnd = 0; // If calculateExtraLayoutSpace is not overridden, call the // deprecated getExtraLayoutSpace for backwards compatibility @SuppressWarnings("deprecation") int extraScrollSpace = getExtraLayoutSpace(state); if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) { extraLayoutSpaceStart = extraScrollSpace; } else { extraLayoutSpaceEnd = extraScrollSpace; } extraLayoutSpace[0] = extraLayoutSpaceStart; extraLayoutSpace[1] = extraLayoutSpaceEnd; }
这里的state是从onMeasure传过来的,记录的是RecyclerView有关的当前信息。
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION && mPendingScrollPositionOffset != INVALID_OFFSET) { // if the child is visible and we are going to move it around, we should layout // extra items in the opposite direction to make sure new items animate nicely // instead of just fading in final View existing = findViewByPosition(mPendingScrollPosition); if (existing != null) { final int current; final int upcomingOffset; if (mShouldReverseLayout) { current = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(existing); upcomingOffset = current - mPendingScrollPositionOffset; } else { current = mOrientationHelper.getDecoratedStart(existing) - mOrientationHelper.getStartAfterPadding(); upcomingOffset = mPendingScrollPositionOffset - current; } if (upcomingOffset > 0) { extraForStart += upcomingOffset; } else { extraForEnd -= upcomingOffset; } } }
这里是计算屏幕之外的layout长度。
接下来执行的detachAndScrapAttachedViews(recycler)方法会把之前的View回收以作复用,在这个方法里会循环调用getChildAt获取View然后调用scrapOrRecycleView方法:
private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view " + viewHolder); } return; } if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else { detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }
这里会通过View的RecyclerView.LayoutParam拿到它的ViewHolder,回收复用都是针对ViewHolder。这里有两种情况,一种是假定短时间内完全不再需要的Item(大胆猜测这是View滚动到离当前屏幕显示positon范围比较远的时候),另一种是暂时不可见但是假定短时间内可能需要显示的Item,比如刚刚滑出屏幕不远的。第一种是直接调用removeViewAt移除View,然后调用recycleViewHolderInternal方法,这个方法里:
if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // 若超出最大长度则把第一个加入的(也就是最久没被使用的)删去 int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { // 跳过最近预先载入的部分 int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = mCachedViews.get(cacheIndex).mPosition; if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; } mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } }
可见会把remove的View的ViewHolder放到mCachedViews中,如果是不合法的项则调用addViewHolderToRecycledViewPool方法,这个方法中又调用getRecycledViewPool().putRecycledView(holder):
public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } scrap.resetInternal(); scrapHeap.add(scrap); }
mScrap中按照viewType保存了一系列的ScrapData,每个ScrapData都有一个mScrapHeap,保存了所以属于该viewtype的ViewHolder,默认这个集合不能超过五个,即每种类型最多保存前五个ViewHolder且不会被新的替换。总结,这里会分两个缓存关卡,首先尝试放在mCachedViews中,否则尝试放在mScrapHeap中。recycleViewHolderInternal方法最后还会调用mViewInfoStore.removeViewHolder(holder)移除mViewInfoStore中的ViewHolder。
第二种是不会移除View的,也就是说在屏幕可见范围之外还是保存了一定数量的View实例(ViewHolder)的。recycler.scrapView(view)如下:
void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) { throw new IllegalArgumentException("Called scrap view with an invalid view." + " Invalid views cannot be reused from scrap, they should rebound from" + " recycler pool." + exceptionLabel()); } holder.setScrapContainer(this, false); mAttachedScrap.add(holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder); } }
这里是分别处理更新的和不需要更新的ViewHolder,前者会放在mChangedScrap中,后者会放在mAttchedScrap。中,mRecyclerView.mViewInfoStore.onViewDetached(viewHolder)会调用removeFromDisappearedInLayout(viewHolder):
void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) { InfoRecord record = mLayoutHolderMap.get(holder); if (record == null) { return; } record.flags &= ~FLAG_DISAPPEARED; }
mLayoutHolderMap猜测是来记录界面上需要显示的项,渲染的时候应该会用到,这里会修改其中每个InfoRecord的flags。
回到onLayoutChildren中,接下来开始填充:
// fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = 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.mExtraFillSpace = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; }
我截取的是一段正序的逻辑代码,就按照我们习惯的从上到下来理解。updateLayoutStateToFillEnd和updateLayoutStateToFillStart是靠mAchorInfo给mLayoutState赋值,fill方法如下:
LayoutChunkResult layoutChunkResult = mLayoutChunkResult; while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); layoutChunk(recycler, state, layoutState, layoutChunkResult); //计算新View添加后的位移量 layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; /** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout */ if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) { //layoutState.mAvailable也会递减 layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } if (stopOnFocusable && layoutChunkResult.mFocusable) { break; } }
layoutChunk用来添加View:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { 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); } } else { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addDisappearingView(view); } else { addDisappearingView(view, 0); } } measureChildWithMargins(view, 0, 0); result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); int left, top, right, bottom; if (mOrientation == VERTICAL) { if (isLayoutRTL()) { right = getWidth() - getPaddingRight(); left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); } else { left = getPaddingLeft(); right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); } if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { bottom = layoutState.mOffset; top = layoutState.mOffset - result.mConsumed; } else { top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; } } else { top = getPaddingTop(); bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { right = layoutState.mOffset; left = layoutState.mOffset - result.mConsumed; } else { left = layoutState.mOffset; right = layoutState.mOffset + result.mConsumed; } } // 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); if (DEBUG) { Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); } // Consume the available space if the view is not removed OR changed if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } result.mFocusable = view.hasFocusable(); }
layoutState.next方法:
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }
先从mScrapList中取,否则走recycler.getViewForPosition,这个方法最终会调用tryGetViewHolderForPositionByDeadline方法,这个方法较长,逻辑就是依次尝试从上面我们看过的复用集合里取,调用顺序即为获取顺序:getChangedScrapViewForPosition、getScrapOrHiddenOrCachedHolderForPosition、getScrapOrCachedViewForId、getRecycledViewPool().getRecycledView、mAdapter.createViewHolder,代表从mChangedScrap、mAttachedScrap、mCachedViews、mRecyclerPool、mAdapter.onCreateViewHolder中获取。
取到或者创建ViewHolder之后就是添加View:
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); } }
addDisappearingView最终会调用addViewInt方法,这个方法里会把之前remove或者detach的重新绑定到父容器上,这就是“添加”之前显示过的且还存在在复用集合中的View。
然后调用measureChildWithMargins方法重新测量child的尺寸,接下来就是计算child的新的layout信息,然后调用layoutDecoratedWithMargins方法重新排列:
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right, int bottom) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect insets = lp.mDecorInsets; child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin); }
-
ItemDecoration
RecyclerView有一个addItemDecoration方法可以添加分割线。
它的使用在onDraw方法中得到体现:
@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); } }
可见,每次onDraw首先会调用super.onDraw方法,说明分割线跟在每一项的后面。调用for循环执行添加的所有ItemDecoration的onDraw方法。
我们以DividerItemDecoration实现类来看看onDraw做了什么:
@Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if (parent.getLayoutManager() == null || mDivider == null) { return; } if (mOrientation == VERTICAL) { drawVertical(c, parent); } else { drawHorizontal(c, parent); } }
这里的mDivider是一个Drawable,mOrientation和RecyclerView的方向是一致的,如果是纵向的就是drawVertival:
private void drawVertical(Canvas canvas, RecyclerView parent) { canvas.save(); final int left; final int right; //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. if (parent.getClipToPadding()) { left = parent.getPaddingLeft(); right = parent.getWidth() - parent.getPaddingRight(); canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom()); } else { left = 0; right = parent.getWidth(); } final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); parent.getDecoratedBoundsWithMargins(child, mBounds); final int bottom = mBounds.bottom + Math.round(child.getTranslationY()); final int top = bottom - mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } canvas.restore(); }
只是这样我们会发现分割线会被画在了child上,我们还需要实现一个getItemOffsets方法,在这里就是:
@Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { if (mDivider == null) { outRect.set(0, 0, 0, 0); return; } if (mOrientation == VERTICAL) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } }
这个方法在哪里调用的呢,在前面onMeasure流程中,调用measureChild方法时:
public void measureChild(@NonNull 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 = getChildMeasureSpec(getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, canScrollHorizontally()); final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, canScrollVertically()); if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); } }
getItemDecorInsetsForChild方法中会调用getItemOffsets:
Rect getItemDecorInsetsForChild(View child) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.mInsetsDirty) { return lp.mDecorInsets; } if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) { // changed/invalid items should not be updated until they are rebound. return lp.mDecorInsets; } 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; }
所以在测量子View的时候就已经把分割线的宽度加入到child的尺寸中去了,在调用getDecoratedBoundsWithMargins的时候拿到的就是预留出分割线宽度的Rect。因为可以添加多个ItemDecoration,所以这里可以在onDraw中有些自定义操作来实现一些想要的效果。
关于dispatchLayoutStep1和dispatchLayoutStep3和RecyclerView的动画关系比较密切,这里先不说他。
-
总结
和ListView相比,RecyclerView有着一些更高级的处理。比如:
- 在架构上使用不同的LayoutManager使其拥有更多的布局模式,而且有必要的话你可以实现自己的LayoutManager;
- 我们不需要额外实现自己的ViewHolder,RecyclerView并不是在滚动的时候即时地去调用Adapter的方法创建ViewHolder,相比入保存View然后通过setTag保存ViewHolder,RecyclerView使用保存ViewHolder的方式来管理View及其相关信息,更加的全面和方便;
- ListView创建View和绑定数据在一个地方,即getView方法,所以getView方法在滚动显示不可见View的时候总是会调用这个方法,而RecyclerView并不是总会创建View,所以他有一个另外的绑定数据的方法;
- ListView在滚动的时候会频繁的addView和removeView,会很影响性能,而RecyclerView会保存一部分屏幕外不可见的View,这样在滚动的时候就会更快的显示,而且设置了最大数量避免引起OOM。
RecyclerView核心原理源码分析
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
- 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
- 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
- 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
推荐阅读更多精彩内容
- 文章篇幅较长,文末有总结和流程图。个人主页:https://chengang.plus/[https://chen...
- 概述当列表数据发生变化时,需要调用Adapter中的一些API来通知RecyclerView改变UI,比如列表刷新...
- 本系列博客基于com.android.support:recyclerview-v7:26.1.01.【进阶】Re...