preface
This paper intends to make a detailed and complete analysis and summary of RecyclerView, because RecycelrView source code is very long (light RecyclerView file itself has 13000+ lines), so the article will be very long, but after an analysis will find, RecyclerView is an enhanced version of ListView, in addition to the use of similar methods, key source code is also very similar.
The use of RecyclerView can refer to daishen’s article:
Android RecyclerView uses fully analytic experience art like controls
Comparative analysis of RecyclerView and ListView
This article uses a “top down” source code analysis — that is, related code is arranged top-down by invocation, methods of the same class are grouped together, and key code is annotated. Nonsense not to say, the following from the RecyclerView drawing process began to analyze.
Drawing process
Starting with the onMeasure method, the key points are annotated:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
Adapter mAdapter;
@VisibleForTesting LayoutManager mLayout;
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if(mLayout isAutoMeasureEnabled ()) {/ / look here for final int widthMode = MeasureSpec. GetMode (widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); // The default value is startif(mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } // dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); //if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if(mLayout.shouldMeasureTwice()) { ... dispatchLayoutStep2(); // now we can get the width and height from the children. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); }}else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
...
} else if (mState.mRunPredictiveAnimations) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return; } mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); . } } void defaultOnMeasure(int widthSpec, int heightSpec) { // calling LayoutManager here is not pretty but that API is already public and it is better // than creating another method since this is internal. final int width = LayoutManager.chooseSize(widthSpec, getPaddingLeft() + getPaddingRight(), ViewCompat.getMinimumWidth(this)); final int height = LayoutManager.chooseSize(heightSpec, getPaddingTop() + getPaddingBottom(), ViewCompat.getMinimumHeight(this));setMeasuredDimension(width, height); } public abstract static class LayoutManager { public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) { mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); }}}Copy the code
MLayout. IsAutoMeasureEnabled () returns a value to false by default, but LinearLayoutManager and StaggeredGridLayoutManager rewrite the method, and the default is true:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
@Override
public boolean isAutoMeasureEnabled() {
return true; }}Copy the code
public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
RecyclerView.SmoothScroller.ScrollVectorProvider {
public static final int GAP_HANDLING_NONE = 0;
public static final int GAP_HANDLING_LAZY = 1;
public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2;
private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
public void setGapStrategy(int gapStrategy) {
...
mGapStrategy = gapStrategy;
requestLayout();
}
@Override
public boolean isAutoMeasureEnabled() {
return mGapStrategy != GAP_HANDLING_NONE;
}
}
Copy the code
The GridLayoutManager inherits from the LinearLayoutManager, which is the three subclasses of Android’s LayoutManager, all using the code in the if branch, The two most important calls to dispatchLayoutStep1 and dispatchLayoutStep2 are dispatchLayoutStep1 and dispatchLayoutStep2.
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
mFirstLayoutComplete = true;
}
/**
* Wrapper around layoutChildren() that handles animating changes caused by layout.
*/
void dispatchLayout() {
mState.mIsMeasuring = false;
if(mstate.mlayoutstep == state.step_start) {// onMeasure already executes step1 and step2. mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); }else if(mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) {// adpter updated, or width or height changed, repeat step2 // First 2 steps aredone in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else{// Set MeasureSpec to EXACTLY // Always make sure we sync them (to ensure mode is exact) mLayout.setExactMeasureSpecsFrom(this); } // dispatchLayoutStep3(); } public abstract static class LayoutManager { voidsetExactMeasureSpecsFrom(RecyclerView recyclerView) {
setMeasureSpecs( MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY) ); }}}Copy the code
It can be found that the layout process of RecyclerView is divided into three steps. The key calls are dispatchLayoutStep1, dispatchLayoutStep2 and dispatchLayoutStep3. The onMeasure method performs the first two steps and the onLayout method performs the last step.
dispatchLayoutStep
dispatchLayoutStep1
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
private void dispatchLayoutStep1() {... processAdapterUpdatesAndSetAnimationFlags();if(mState. MRunSimpleAnimations) {/ / Step 0: find all not delete the item location, ready to execute a layout int count = mChildHelper. GetChildCount ();for(int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(...) ; mViewInfoStore.addToPreLayout(holder, animationInfo); . }}if(mState.mRunPredictiveAnimations) { // Step 1: Save old positions so that LayoutManager can run its mapping logic.saveoldpositions (); final boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged =false; // Temporarily complete the layout process with LayoutManagerdisable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if(! mViewInfoStore.isInPreLayout(viewHolder)) { ... final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(...) ;if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
// we don't process disappearing list because they may re-appear in post layout pass. clearOldPositions(); } else { clearOldPositions(); } mState.mLayoutStep = State.STEP_LAYOUT; } private void processAdapterUpdatesAndSetAnimationFlags () {/ / default is false, The measure for the first time will not perform the if (mDataSetHasChangedAfterLayout) {mAdapterHelper. Reset (); if (mDispatchItemsChangedEvent) { mLayout.onItemsChanged(this); } // The code in the RecyclerView interface Callback method is used in the RecyclerView // Last may call requestLayout walk again drawing process to achieve the effect of the animation if (predictiveItemAnimationsEnabled ()) {mAdapterHelper. The preProcess (); } else { mAdapterHelper.consumeUpdatesInOnePass(); } mState.mRunSimpleAnimations = mFirstLayoutComplete && ... ; mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && ! mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); } static ViewHolder getChildViewHolderInt(View child) { if (child == null) { return null; } return ((LayoutParams) child.getLayoutParams()).mViewHolder; }}Copy the code
According to the dispatchLayoutStep1 comments and code, this method is responsible for:
- Perform adapter updates, which may end up calling requestLayout
- Decide which animations should be executed (not yet)
- Save information about the child View
- Perform pre-layout if necessary
If it is the first time to execute the measure process, the Adapter normally has no element that can be updated, that is, the function of dispatchLayoutStep1 is mainly to calculate and save the information related to the child View and animation.
dispatchLayoutStep2
Whether to execute the pre-layout involves many variables, so ignore it for now. Now continue with dispatchLayoutStep2:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {... // Use LayoutManager to complete the actual layout // Step 2: Run Layout mstate.minpreLayout =false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false; . }}Copy the code
As you can see, dispatchLayoutStep2 is much simpler than dispatchLayoutStep1. It does the actual layout. It calls mLayout.onLayoutChildren. This method is an empty implementation in the LayoutManager, its three subclasses LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager has its own implementation. Here’s an example of the LinearLayoutManager (the key parts, like the caching mechanism, are the same for all three layoutManagers) :
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
@Override
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.
// create layout state
if(mPendingSavedState ! = null || mPendingScrollPosition ! = NO_POSITION) {if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return; }}... Final View focused = getFocusedChild();if(! mAnchorInfo.mValid || mPendingScrollPosition ! = NO_POSITION || mPendingSavedState ! = null) { updateAnchorInfoForLayout(recycler, state, mAnchorInfo); }else if(...). { mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); }if(state.isPreLayout() && mPendingScrollPosition ! = 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. } onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); / / traverse to the child View detach off and cache into the Recycler detachAndScrapAttachedViews (Recycler); mLayoutState.mIsPreLayout = state.isPreLayout();if(mAnchorInfo.mLayoutFromEnd) { // fill towards start updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; // View fill(recycler, mLayoutState, state,false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; }}else{ // fill towards end ... // fill towards start ... }... layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);if(! state.isPreLayout()) { mOrientationHelper.onLayoutComplete(); }else{ mAnchorInfo.reset(); }... }}Copy the code
The onLayoutChildren function is to load the child View after finding the anchor point coordinates and anchor point positions of the child View. Anchor point coordinates and the calculation method of oversight, the focus on detachAndScrapAttachedViews and fill these two methods.
detachAndScrapAttachedViews
This method will not work in the first measure because RecyclerView does not have child views. And in the second and third layout, it will remove or detach the child View from RecyclerView, and cache the child View, so that later add back or attach back, Avoid reloading the same child View — listViews are almost identical:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
public abstract static class LayoutManager {
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);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! MRecyclerView. MAdapter. HasStableIds ()) {/ / remove the View and use mCacheVies son or RecycledViewPool cache removeViewAt (index); recycler.recycleViewHolderInternal(viewHolder); }else{// Detach subview and cache detachViewAt(index) with mChangedScarp or mAttachedScarp; recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }} // Add/remove and attach/ detach are done via mChildHelper. Callback can be instantiated by RecyclerView's initChildrenHelper and/or ViewGroup's add/remove and attach/detach public void removeViewAt(int index) { final View child = getChildAt(index);if(child ! = null) { mChildHelper.removeViewAt(index); } } public void detachViewAt(int index) { detachViewInternal(index, getChildAt(index)); } private void detachViewInternal(int index, View view) { mChildHelper.detachViewFromParent(index); } } /** * A Recycler is responsiblefor managing scrapped or detached item views for reuse.
*
* <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.</p>
*
* <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
* an adapter's data set representing the data at a given position or item ID. * If the view to be reused is considered "dirty" the adapter will be asked to rebind it. * If not, the view can be quickly reused by the LayoutManager with no further work. * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout} * may be repositioned by a LayoutManager without remeasurement. */ public final class Recycler { final ArrayList
mAttachedScrap = new ArrayList<>(); ArrayList
mChangedScrap = null; final ArrayList
mCachedViews = new ArrayList
(); private final List
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); static final int DEFAULT_CACHE_SIZE = 2; private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; Int mViewCacheMax = DEFAULT_CACHE_SIZE; // mCachedVies can put up to 2. This limit is applied when mCachedView calls add. RecycledViewPool mRecyclerPool; private ViewCacheExtension mViewCacheExtension; void recycleViewHolderInternal(ViewHolder holder) { ... boolean cached = false; boolean recycled = false; If (forceRecycle | | holder. IsRecyclable ()) {/ / if conditions are met, just on mCachedViews inside the if (mViewCacheMax > 0 &&! holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view int cachedViewSize = mCachedViews.size(); // Cache capacity limit is exceeded, RecycledViewPool if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {recycleCachedViewAt(0); cachedViewSize--; }... McAchedviews. add(targetCacheIndex, holder); cached = true; // Create a RecycledViewPool if (! cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { ... } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); } // Delete a View from mCachedViews, Void recycleCachedViewAt(int cachedViewIndex) {ViewHolder ViewHolder = mCachedViews.get(cachedViewIndex); addViewHolderToRecycledViewPool(viewHolder, true); mCachedViews.remove(cachedViewIndex); } void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) { ... getRecycledViewPool().putRecycledView(holder); } /** * Mark an attached view as scrap. */ void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { mAttachedScrap.add(holder); } else { mChangedScrap.add(holder); } } } /** * RecycledViewPool lets you share Views between multiple RecyclerViews. */ public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; Static class ScrapData {// mScrapHeap final ArrayList
mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } View SparseArray
mScrap = new SparseArray<>(); private int mAttachCount = 0; public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList
scrapHeap = getScrapDataForType(viewType).mScrapHeap; scrapHeap.add(scrap); } private ScrapData getScrapDataForType(int viewType) { ScrapData scrapData = mScrap.get(viewType); if (scrapData == null) { scrapData = new ScrapData(); mScrap.put(viewType, scrapData); } return scrapData; }}}
Copy the code
As you can see, detachAndScrapAttachedViews has touched the way this humble RecyclerView core, its action can be summed up in one word: remove/detach all the View, and cache them. If the View can be removed according to the ViewHolder state, remove the View and cache it into the Recyclable mCachedViews array. By default, mCachedViews can cache only two. If mCachedViews exceeds its capacity, remove the oldest item from the mCachedViews array and place it in the RecycledViewPool’s mScrapHeap array. If we can’t remove the recyclable mChangedScrap, detach the recyclable View and cache it into the Recyclable mChangedScrap array. Otherwise cache Recycler’s mAttachedScrap array.
The difference between Recycler and RecycledViewPool is that each Recycler has a RecyclerView; A RecycledViewPool can correspond to multiple RecyclerViews. RecycledViewPool inside the View can be provided to multiple RecyclerView reuse.
Scrap View represents a View that has been removed for reuse. If the View to be reused is marked as dirty, the Adapter needs to be rebound, otherwise LayoutManager can use it directly.
DetachAndScrapAttachedViews analysis here, then continue to analyze onLayoutChildren. The next call of interest in the onLayoutChildren method is fill.
fill
The fill method is especially critical – if detachAndScrapAttachedViews touched RecyclerView core, then the fill method is the core of the real. It loads child views from Adapter, recycles views from recycle pool, and displays key calls to RecyclerView. The source author calls it magic method:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
/**
* The magic functions:). Fills the given layout, defined by the layoutState. This is fairly * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager} * and with little change, can be made publicly available as a helper class. */ int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { ... / / thiswhileThe loop is the most critical part, and it shows that RecyclerView, like ListView, only loads a child View that can be displayed on the phone screenwhile((layoutState mInfinite | | remainingSpace > 0) && layoutState. HasMore (state)) {/ / load the child View layoutChunk (recycler, state, layoutState, layoutChunkResult); /** * Consume the available spaceif: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout * /if(! layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList ! = null || ! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is importantforrecycling remainingSpace -= layoutChunkResult.mConsumed; } // If you are currently scrollingif(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if(layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; // Recycle recycleByLayoutState(Recycler, layoutState); // Recycle recycleByLayoutState(Recycler, layoutState); }if (stopOnFocusable && layoutChunkResult.mFocusable) {
break; }}returnstart - layoutState.mAvailable; } void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {// Get a View from Adapter or recycle pool View View = layoutState. Next (recycler);if (view == null) {
result.mFinished = true;
return; } LayoutParams params = (LayoutParams) view.getLayoutParams(); // Add a child View to RecyclerViewif (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); . // 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); result.mFocusable = view.hasFocusable(); } static class LayoutState { List
mScrapList = null; /** * Gets the view for the next element that we should layout. * Also updates current item index to the next item, based on {@link #mItemDirection} */ View next(RecyclerView.Recycler recycler) { if (mScrapList ! Return nextViewFromScrapList(); return nextViewFromScrapList(); } / / to get a View from Adapter or recycling pool final View View = recycler. GetViewForPosition (mCurrentPosition); mCurrentPosition += mItemDirection; return view; }}}
Copy the code
As you can see, the fill method loops in and gets a child View and loads it onto the screen until there’s no more space or any more child views. This is done by layoutChunk, which first calls getViewForPosition to try to get a child View, Then call addView method to load it into RecyclerView, and finally execute the measure process of sub-view. At the same time, it determines whether RecyclerView is scrolling based on LayoutState, and if so, it caches out-of-screen child Views for later reuse. Here’s how it works, starting with getViewForPosition:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
public final class Recycler {
/**
* Obtain a view initialized for the given position.
*
* This method should be used by {@link LayoutManager} implementations to obtain
* views to represent data from an {@link Adapter}.
* <p>
* The Recycler may reuse a scrap or detached view from a shared pool if one is
* available for the correct view type. If the adapter has not indicated that the
* data at the given position has changed, the Recycler will attempt to hand back
* a scrap view that was previously initialized for that data without rebinding.
*/
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; // 0) If there is a changed scrap, try to find from thereif(mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! = null; } // 1) Find by position from Scrap /hidden list/cacheif (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if(holder ! = null) {if(! validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrapif relevant) since it can't be used
// 这个 View 会被重用,从缓存池中移除它
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
}
if (!dryRun) {
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) 如果上一步没有获取到,则根据 id 依次从 mAttachedScrap, mCachedViews 中获取
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
// 和 getScrapOrHiddenOrCachedHolderForPosition 类似,省略具体代码
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
if (holder == null && mViewCacheExtension != null) {
// 从 mViewCacheExtension 中获取
// We are NOT sending the offsetPosition because LayoutManager does not know it.
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
...
}
}
if (holder == null) { // fallback to pool
// 还没有获取到,则尝试从 RecycledViewPool 中获取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
}
}
if (holder == null) {
// 没有可以复用的 View,则回调 mAdapter 的 onCreateViewHolder 方法加载一个子 View
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// 如果是预布局,则设置子 View 的 position 信息即可
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 否则回调 Adapter 的 onBindViewHolder 方法,为子 View 绑定数据
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
ViewHolder getChangedScrapViewForPosition(int position) {
...
// find by position
for (int i = 0; i < changedScrapSize; i++) {
// 从 mChangedScrap 中获取缓存的 View
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
...
}
return null;
}
/**
* Returns a view for the position either from attach scrap, hidden children, or cache.
*
* @param dryRun Does a dry run, finds the ViewHolder but does not remove
*/
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) {
// 作用是把之前隐藏的 View 重新拿出来,这里不把它算作缓存机制的一部分
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
...
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
// 从缓存池中移除,getViewForPosition 中传过来的 dryRun 值为 false
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
}
return null;
}
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
...
mAdapter.bindViewHolder(holder, offsetPosition);
...
return true;
}
}
public abstract static class Adapter<VH extends ViewHolder> {
private final AdapterDataObservable mObservable = new AdapterDataObservable();
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
...
// 从 xml 资源中加载子 View 并创建 ViewHolder,这是我们重写 Adapter 时的关键方法之一
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
return holder;
...
}
public final void bindViewHolder(@NonNull VH holder, int position) {
holder.mPosition = position;
if (hasStableIds()) {
holder.mItemId = getItemId(position);
}
// 绑定数据到 ViewHolder,这是我们重写 Adapter 时的关键方法之一
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
...
}
}
}
Copy the code
As can be seen, getViewForPosition is to try to obtain Recycler or RecycledViewPool one by one in a fixed order: MChangedScrap -> mAttachedScrap -> mCachedViews -> mViewCacheExtension -> RecycledViewPool The Adapter’s onCreateViewHolder method is called to load the child View from the XML resource.
After successfully obtaining an itemView, if it is currently in the pre-layout stage, you can set the location information of the itemView. Otherwise, call the Adapter’s onBindViewHolder to bind the data to the itemView. Then you can use addView to add an itemView to RecyclerView:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 { public abstract static class LayoutManager { public void addView(View child) { addView(child, - 1); } public void addView(View child, int index) { addViewInt(child, index,false);
}
public void addDisappearingView(View child) {
addDisappearingView(child, -1);
}
public void addDisappearingView(View child, int index) {
addViewInt(child, index, true);
}
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if(holder. WasReturnedFromScrap () | | holder. IsScrap ()) {/ / from the cache pool to take out the View, The attach can mChildHelper. AttachViewToParent (child, index, child. GetLayoutParams (),false);
} else if(child.getparent () == mRecyclerView) {// It was not a scrap but a valid child // It was not a scrap but a valid childif (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} elseMcHildhelper. addView(child, index,false);
}
if (lp.mPendingInvalidate) {
holder.itemView.invalidate();
lp.mPendingInvalidate = false; } } /** * Moves a View from one position to another. */ public void moveView(int fromIndex, int toIndex) { View view = getChildAt(fromIndex); detachViewAt(fromIndex); attachView(view, toIndex); }}}Copy the code
As mentioned earlier, if the screen is currently scrolling, fill also calls recycleByLayoutState to cache out-of-screen child views:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if(! layoutState.mRecycle || layoutState.mInfinite) {return; } // Calculate which views need to be removed based on the distance of the slideif (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) {
final int childCount = getChildCount();
final int limit = mOrientationHelper.getEnd() - dt;
if (mShouldReverseLayout) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {// cache // Stop here recycleChildren(recycler, 0, I);return; }}}else{... } } private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { ... // Remove and cacheif (endIndex > startIndex) {
for(int i = endIndex - 1; i >= startIndex; i--) { removeAndRecycleViewAt(i, recycler); }}else {
for(int i = startIndex; i > endIndex; i--) { removeAndRecycleViewAt(i, recycler); }}}}Copy the code
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 { public abstract static class LayoutManager { public void removeAndRecycleViewAt(int index, Recycler recycler) { final View view = getChildAt(index); // Remove child View removeViewAt(index) from RecyclerView; // Remove cache recycler.recycleView(view); } public void removeViewAt(int index) { final View child = getChildAt(index);if(child ! = null) { mChildHelper.removeViewAt(index); } } } public final class Recycler { 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. evenif it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if(holder.isScrap()) {// Remove ViewHolder from mChangedScrap or mAttachedScrap holder.unscrap (); }else if(holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } / / this method analysis, before used to cache the ViewHolder to mCachedViews recycleViewHolderInternal (holder); } void unscrapView(ViewHolder holder) {// Remove mChangedScrap or mAttachedScrapif (holder.mInChangeScrap) {
mChangedScrap.remove(holder);
} else {
mAttachedScrap.remove(holder);
}
holder.mScrapContainer = null;
holder.mInChangeScrap = false;
holder.clearReturnedFromScrapFlag();
}
}
public abstract static class ViewHolder {
private Recycler mScrapContainer = null;
void unScrap() { mScrapContainer.unscrapView(this); }}}Copy the code
As you can see, fill will eventually cache the child View that is removed from the screen in mCahcedViews. If the View is already cached in mChangedScrap or mAttachedScrap, it will also be removed. Move to mCahcedViews.
As can be seen from the above analysis, the working mechanism of RecyclerView and ListView is very similar:
- The same method that calls back the Adapter on the first layout loads the child View from the XML resource
- In the second and third layout, remove/detach all the sub-views first, and then add/attach back at last, in order to avoid loading the same sub-views repeatedly
- Again, load only one sub-view that can be displayed on the phone screen
- Also use multiple cache array/list, and different cache array/list corresponding function is different, RecylerView mAttachedScrap corresponding to ListView mActiveViews, mCahcedViews corresponding to mScrapViews
The difference is:
- In addition to attach/detach, RecyclerView may remove child views and add back, ListView will only detach and attach
- RecyclerView’s sub-view cache list has a more detailed division of work, with 5 levels of cache, namely, mChangedScrap, mAttachedScrap, mCahcedViews, mViewCacheExtension, RecycledViewPool, Wherein mViewCacheExtension is used to provide user custom cache logic, and RecycledViewPool can even provide multiple RecyclerView common; ListView is only divided into mActiveViews and mScrapViews
- The cache unit of RecyclerView is ViewHolder, and the cache unit of ListView is View. ListView needs to cooperate with setTag method to realize reuse
The basic analysis of RecyclerView cache mechanism is completed here. Continue to see dispatchLayoutStep3 below.
dispatchLayoutStep3
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
/**
* The final step of the layout where we save the information about views for animations,
* trigger animations and do any necessary cleanup.
*/
private void dispatchLayoutStep3() {... // Reset LayoutState mstate. mLayoutStep = state.step_start;if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if(oldChangeViewHolder ! = null && ! oldChangeViewHolder.shouldIgnore()) { // run a change animationif (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// we add and remove so that any post info is merged.
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else{// Implement animation animateChange(oldChangeViewHolder, Holder, preInfo, postInfo, oldBsellers, newbsellers); }}}else{ mViewInfoStore.addToPostLayout(holder, animationInfo); } // Execute the animation in mViewInfoProcessCallback. Process view info lists and trigger animations mViewInfoStore.process(mViewInfoProcessCallback); } / / move in View of cache mAttachedScrap mCachedViews mLayout inside. RemoveAndRecycleScrapInt (mRecycler);if(mRecycler.mChangedScrap ! = null) { mRecycler.mChangedScrap.clear(); }if(mLayout mPrefetchMaxObservedInInitialPrefetch) {/ / move mCachedViews redundant View RecycledViewPool inside mRecycler.updateViewCacheSize(); } mLayout.onLayoutCompleted(mState); onExitLayoutOrScroll(); stopInterceptRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
dispatchOnScrolled(0, 0);
}
recoverFocusFromState();
resetFocusInfo();
}
private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
boolean oldHolderDisappearing, boolean newHolderDisappearing) {
...
if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
postAnimationRunner();
}
}
/**
* The callback to convert view info diffs into animations.
*/
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
new ViewInfoStore.ProcessCallback() { @Override public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, @Nullable ItemHolderInfo postInfo) { mRecycler.unscrapView(viewHolder); animateDisappearance(viewHolder, info, postInfo); } @Override public void processAppeared(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo info) { animateAppearance(viewHolder, preInfo, info); } // Various animations end up calling postAnimationRunner... }; void animateDisappearance(@NonNull ViewHolder holder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
/**
* Post a runnable to the next frame to run pending item animations. Only the first such
* request will be posted, governed by the mPostedAnimatorRunner flag.
*/
void postAnimationRunner() {
if(! mPostedAnimatorRunner && mIsAttached) { ViewCompat.postOnAnimation(this, mItemAnimatorRunner); mPostedAnimatorRunner =true; ItemAnimator mItemAnimator = new DefaultItemAnimator(); private Runnable mItemAnimatorRunner = newRunnable() {
@Override
public void run() {
if(mItemAnimator ! = null) { mItemAnimator.runPendingAnimations(); } mPostedAnimatorRunner =false; }}; public abstract static class LayoutManager { /** * Recycles the scrapped views. */ void removeAndRecycleScrapInt(Recycler recycler) { final int scrapCount = recycler.getScrapCount(); // Loop backward, recycler might be changed by removeDetachedView()for (int i = scrapCount - 1; i >= 0; i--) {
final View scrap = recycler.getScrapViewAt(i);
final ViewHolder vh = getChildViewHolderInt(scrap);
if (vh.isTmpDetached()) {
mRecyclerView.removeDetachedView(scrap, false);
}
if(mRecyclerView.mItemAnimator ! = null) { mRecyclerView.mItemAnimator.endAnimation(vh); } / / into the inside mCachedViews recycler. QuickRecycleScrapView (scrap); } recycler.clearScrap();if (scrapCount > 0) {
mRecyclerView.invalidate();
}
}
int getScrapCount() {
return mAttachedScrap.size();
}
View getScrapViewAt(int index) {
returnmAttachedScrap.get(index).itemView; }}}Copy the code
The main function of dispatchLayoutStep3 is to perform animation and do some cleanup. The default animation executor is DefaultItemAnimator. The cleanup includes moving the views cached in mAttachedScrap to mCachedViews. If the capacity of mCachedViews exceeds the limit, move some of the views to RecycledViewPool. Clean all elements of mChangedScrap at the same time.
Slide to load more data
After the above analysis, it is basically certain that RecyclerView will call fill method to use mCachedViews to cache and reuse sub-views when it slides to load more data. Here is the verification:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
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; . }return true;
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
if(mAdapter ! = null) { ... // Event handling for scrolling is done by LayoutManagerif(x ! = 0) { consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState); unconsumedX = x - consumedX; }if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
}
if(! mItemDecorations.isEmpty()) { invalidate(); }if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
...
} else if(getOverScrollMode() ! = View.OVER_SCROLL_NEVER) { ... }returnconsumedX ! = 0 || consumedY ! = 0; } // If the Adapter is updated, go through the Layout process void againconsumePendingUpdateOperations() {
if(! mFirstLayoutComplete || mDataSetHasChangedAfterLayout) { dispatchLayout();return;
}
if(! mAdapterHelper.hasPendingUpdates()) {return; } / /if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any
// of the visible items is affected and if not, just ignore the change.
if(mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && ! mAdapterHelper .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE | AdapterHelper.UpdateOp.MOVE)) {if(! mLayoutWasDefered) {if (hasUpdatedView()) {
dispatchLayout();
} else{ // no need to layout, clean state mAdapterHelper.consumePostponedUpdates(); }}}else if(mAdapterHelper.hasPendingUpdates()) { dispatchLayout(); }}}Copy the code
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true; . // Fill was analyzed above, that is, loop through the child View until the screen is filled, Or a child View consumed final int consumed. = mLayoutState mScrollingOffset + the fill (recycler, mLayoutState, state,false); .returnscrolled; }}Copy the code
As can be seen, as speculated above, RecyclerView will call fill method in the sliding process, and use mCachedViews to cache and reuse sub-views. If Adapter is updated, it will go through the layout process again.
Adapter
The above can be said to have completed the most critical part of RecyclerView analysis, but there are still some problems are not solved, that is, how RecyclerView is updated Adapter? Let’s start analyzing this problem.
Start with the setAdapter method of RecyclerView:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
Adapter mAdapter;
AdapterHelper mAdapterHelper;
public void setAdapter(Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false.true);
processDataSetCompletelyChanged(false);
requestLayout();
}
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
...
mAdapter = adapter;
if(adapter ! = null) { adapter.registerAdapterDataObserver(mObserver); adapter.onAttachedToRecyclerView(this); }... } void processDataSetCompletelyChanged(boolean dispatchItemsChanged) { mDispatchItemsChangedEvent |= dispatchItemsChanged; / / tag DataSet changed, after will call dispatchLayout mDataSetHasChangedAfterLayout = go over the layout processtrue; markKnownViewsInvalid(); } public abstract static class Adapter<VH extends ViewHolder> {private final AdapterDataObservable mObservable = new AdapterDataObservable(); @NonNull public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); public abstract void onBindViewHolder(@NonNull VH holder, int position); . / / registered observers public void registerAdapterDataObserver (@ NonNull AdapterDataObserver observer) { mObservable.registerObserver(observer); } public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { mObservable.unregisterObserver(observer); } // Notify the observer that the data has changed public final voidnotifyDataSetChanged() { mObservable.notifyChanged(); } public final void notifyItemInserted(int position) { mObservable.notifyItemRangeInserted(position, 1); }... } static class AdapterDataObservable extends Observable<AdapterDataObserver> {// Notify all observers public voidnotifyChanged() {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
public void notifyItemRangeInserted(int positionStart, int itemCount) {
for(int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeInserted(positionStart, itemCount); }}... } private class RecyclerViewDataObserver extends AdapterDataObserver {RecyclerViewDataObserver() {
}
@Override
public void onChanged() {
processDataSetCompletelyChanged(true);
if(! mAdapterHelper.hasPendingUpdates()) { requestLayout(); } } @Override public void onItemRangeInserted(int positionStart, int itemCount) {if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
void triggerUpdateProcessor() {// If mHasFixedSize is set, some work may be omittedif (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else{/ / or you need to go again mAdapterUpdateDuringMeasure = the View of the three major processtrue;
requestLayout();
}
}
}
final Runnable mUpdateChildViewsRunnable = new Runnable() {
@Override
public void run() {
if(! mFirstLayoutComplete || isLayoutRequested()) { // a layout request will happen, we should notdo layout here.
return;
}
if(! mIsAttached) { requestLayout(); //if we are not attached yet, mark us as requiring layout and skip
return;
}
if (mLayoutFrozen) {
mLayoutWasDefered = true;
return; //we'll process updates when ice age ends. } consumePendingUpdateOperations(); }}; Void consumePendingUpdateOperations () {/ / perform the dispatchLayoutStep1 again, dispatchLayoutStep2, dispatchLayoutStep3 if (! mFirstLayoutComplete || mDataSetHasChangedAfterLayout) { dispatchLayout(); return; } if (! mAdapterHelper.hasPendingUpdates()) { return; } // if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any // of the visible items is affected and if not, just ignore the change. if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && ! mAdapterHelper .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE | AdapterHelper. UpdateOp. MOVE)) {/ / local/update/end call AdapterHelper Callback methods, The implementation of the Callback method can check RecyclerView initAdapterManager mAdapterHelper. The preProcess (); if (! mLayoutWasDefered) { if (hasUpdatedView()) { dispatchLayout(); } else { // no need to layout, clean state mAdapterHelper.consumePostponedUpdates(); } } } else if (mAdapterHelper.hasPendingUpdates()) { dispatchLayout(); }}}Copy the code
According to the above analysis, it can be known that:
- The relationship between RecyclerView and Adapter is between the observer and the observed. When Adapter calls notifyDataSetChanged and other methods, RecyclerView can know the change and execute the corresponding behavior such as layout
- If the ReccylerView is set to mHasFixedSize, then local RecyclerView updates can be performed when the Adapter is modified to avoid going through the three processes again and improve efficiency
conclusion
Measure and layout of RecyclerView are divided into three steps:
- DispatchLayoutStep1, Adpater updates, calculates and saves information about subviews and animations, pre-layout (if necessary)
- DispatchLayoutStep2, responsible for the actual layout, the specific work is handed to LayoutManager to complete, but the basic process is the same, have the same sub-view reuse mechanism
- DispatchLayoutStep3 is responsible for performing animation and cleanup. The default animation executor is DefaultItemAnimator
RecyclerView and ListView:
- The same method that calls back the Adapter on the first layout loads the child View from the XML resource
- In the second and third layout, remove/detach all the sub-views first, and then add/attach back at last, in order to avoid loading the same sub-views repeatedly
- Again, load only one sub-view that can be displayed on the phone screen
- Also use multiple cache array/list, and different cache array/list corresponding function is different, RecylerView mAttachedScrap corresponding to ListView mActiveViews, mCahcedViews corresponding to mScrapViews
- The observer pattern is also used to make changes when the Adapter calls methods such as notifyDataSetChanged
The difference is:
- In addition to attach/detach, RecyclerView may remove child views and add back, ListView will only detach and attach
- RecyclerView’s sub-view cache list has a more detailed division of work, with 5 levels of cache, namely, mChangedScrap, mAttachedScrap, mCahcedViews, mViewCacheExtension, RecycledViewPool, Wherein mViewCacheExtension is used to provide user custom cache logic, and RecycledViewPool can even provide multiple RecyclerView common; ListView is only divided into mActiveViews and mScrapViews
- The cache unit of RecyclerView is ViewHolder, and the cache unit of ListView is View. ListView needs to cooperate with setTag method to realize reuse