Opening:
To understand RecyclerView from its overall start to see, to prevent a glimpse of the visible, see the wood for the trees.
RecyclerView family tree
As can be seen from the above figure, RecyclerView design is very flexible, it split its own functions into several modules, so that developers can flexibly configure according to their own needs.
RecyclerView basic usage
We start from the basic usage of RecyclerView, began to understand RecyclerView.
Recycler_view.layoutmanager = recycler_view.layoutManager = recycler_view.layoutManager = MyAdapter() LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)Copy the code
The source code to track
From the above we can see that the use of RecyclerView is very simple, only according to a few lines of code can complete the configuration and use of RecyclerView, so let’s start from setLayoutManager trace back to the source, look at the mystery of RecyclerView.
// RecyclerView#setLayoutManager(@nullable LayoutManager layout) public void setLayoutManager(@nullable LayoutManager layout LayoutManager layout) { if (layout == mLayout) { return; } stopScroll(); // TODO We should do this switch a dispatchLayout pass and animate children. There is a good // chance that LayoutManagers will re-use views. // If a LayoutManager has been associated with it before the setLayoutManager method is called, then the old LayoutManager is cleared (mLayout ! = null) { // end all running animations if (mItemAnimator ! = null) { mItemAnimator.endAnimations(); } mLayout.removeAndRecycleAllViews(mRecycler); mLayout.removeAndRecycleScrapInt(mRecycler); mRecycler.clear(); if (mIsAttached) { mLayout.dispatchDetachedFromWindow(this, mRecycler); } mLayout.setRecyclerView(null); mLayout = null; } else { mRecycler.clear(); } // this is just a defensive measure for faulty item animators. mChildHelper.removeAllViewsUnfiltered(); mLayout = layout; // Check whether the new configuration of LayoutManager is associated with a RecyclerView, if so, throw an exception if (layout! = null) { if (layout.mRecyclerView ! = null) { throw new IllegalArgumentException("LayoutManager " + layout + " is already attached to a RecyclerView:" + layout.mRecyclerView.exceptionLabel()); } mLayout.setRecyclerView(this); if (mIsAttached) { mLayout.dispatchAttachedToWindow(this); } } mRecycler.updateViewCacheSize(); SetLayoutManager (); setLayoutManager (); setLayoutManager (); }Copy the code
Then can only then analyze RecyclerView onMeasure and onLayout process
@Override protected void onMeasure(int widthSpec, Int heightSpec (mLayout ==) {// If (mLayout ==); // If (mLayout ==); // If (mLayout ==); // If (mLayout ==) null) { defaultOnMeasure(widthSpec, heightSpec); return; } / / if set LayoutManager is opens the automatic measurement (a flag), such as LinearLayoutManager will return true if (mLayout. IsAutoMeasureEnabled ()) {final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); /** * This specific call should be considered deprecated and replaced with * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could * break existing third party code but all documentation directs developers to not * override {@link LayoutManager#onMeasure(int, Int)} when * {@link LayoutManager#isAutoMeasureEnabled()} returns true // Recycle can be RecyclerView or recycle can be RecyclerView. OnMeasure (mRecycler, mState, widthSpec, heightSpec); Final Boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) { return; // mState is created at the same time as RecyclerView is created (initialized) // Where mLayoutStep has three values (STEP_START, STEP_LAYOUT, STEP_ANIMATIONS) is The if (mstate.mlayoutstep == state.step_start) {/** * The first step of calling The corresponding dispatchLayoutStep123 series of methods 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 */ / 解 析 : Collect animation items and animation information before the layout begins State.step_layout: dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; MLayoutStep = state.step_animations // This method will result in mstate.mlayoutStep = state.step_animations, indicating that the layout is complete and can be animated. This method will be highlighted below 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, // dispatchLayoutStep2 is called twice to get the width and height of RecyclerView if there is no RecyclerView and at least one child View has no RecyclerView width and height (mLayout.shouldMeasureTwice()) { mLayout.setMeasureSpecs( MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); mState.mIsMeasuring = true; 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) { startInterceptRequestLayout(); onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); onExitLayoutOrScroll(); if (mState.mRunPredictiveAnimations) { mState.mInPreLayout = true; } else { // consume remaining updates to provide a consistent state with the layout pass. mAdapterHelper.consumeUpdatesInOnePass(); mState.mInPreLayout = false; } mAdapterUpdateDuringMeasure = false; stopInterceptRequestLayout(false); } else if (mState.mRunPredictiveAnimations) { // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true: // this means there is already an onMeasure() call performed to handle the pending // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time // because getViewForPosition() will crash when LM uses a child to measure. setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()); return; } if (mAdapter ! = null) { mState.mItemCount = mAdapter.getItemCount(); } else { mState.mItemCount = 0; } startInterceptRequestLayout(); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); stopInterceptRequestLayout(false); mState.mInPreLayout = false; // clear } }Copy the code
Now that the if branch tracing of the onMeasure is over, let’s call the onLayout method
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); // The onLayout method does nothing but call dispatchLayout(). TraceCompat.endSection(); mFirstLayoutComplete = true; } void dispatchLayout() { if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); // leave the state in START return; } if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START return; } mState.mIsMeasuring = false; If (mstate.mlayoutStep == state.step_start) if (mstate.mlayoutStep == state.step_start) { dispatchLayoutStep1(); mLayout.setExactMeasureSpecsFrom(this); DispatchLayoutStep2 (); dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) { // First 2 steps are done in onMeasure but looks like we have to run again due to // changed size. mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { // always make sure we sync them (to ensure mode is exact) mLayout.setExactMeasureSpecsFrom(this); } // dispatchLayoutStep3 dispatchLayoutStep3(); }Copy the code
The focus is on tracing the dispatchLayoutStep2 method, which is the beginning of the measurement sub-view
/** * 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() { startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); mState.mItemCount = mAdapter.getItemCount(); mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; // Step 2: Run layout mState.mInPreLayout = false; // The RecyclerView delegate the measurement and layout work to the LayoutManager, but there is no implementation in the LayoutManager, if the custom LayoutManager must implement this method mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = false; mPendingSavedState = null; // onLayoutChildren may have caused client code to disable item animations; re-check mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator ! = null; mState.mLayoutStep = State.STEP_ANIMATIONS; onExitLayoutOrScroll(); stopInterceptRequestLayout(false); }Copy the code
Since onLayoutChildren of LayoutManager is an empty implementation, we take LinearLayoutManager as an example to carry out tracking analysis
// LinearLayoutManager#onLayoutChildren @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... // Here Recyclerview this component to consider the state of Recyclerview is recycled if (mPendingSavedState! = null || mPendingScrollPosition ! = RecyclerView.NO_POSITION) { if (state.getItemCount() == 0) { removeAndRecycleAllViews(recycler); return; } } if (mPendingSavedState ! = null && mPendingSavedState.hasValidAnchor()) { mPendingScrollPosition = mPendingSavedState.mAnchorPosition; } // This method creates the LayoutState object // The LayoutState object stores the key information of the LayoutManager in its sliding state // (e.g., how much free screen space is left on the mAvailable screen, the number of items mCurrentPosition is currently in, the offset of the current slide of mScrollingOffset, etc.) // When an item is filled to the list, it is determined by this object information ensureLayoutState(); mLayoutState.mRecycle = false; / / resolve layout direction / / reverse layout: such as qq chat list, chat is always displayed at the bottom of the latest news resolveShouldLayoutReverse (); final View focused = getFocusedChild(); // The AnchorInfo layout is an anchor View, and the first time manchorinfo.mvalid is false, the if branch is entered if (! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! = null) { mAnchorInfo.reset(); mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; Calculate anchor position and coordinate // Collect anchor point View coordinates and position information // Anchor point View may obtain the current anchor point from list recovery data // or the View with the focus item // The anchor View fills the layout up from the anchor position, and then up from the anchor position. This is the LinearLayoutManager fill strategy 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)); } // LLM may decide to layout items for "extra" pixels to account for scrolling target, // the LayoutManager#scrollToPosition method is called at the beginning of the layout, so offsets are calculated... // Focus on recycling ViewHolder before recyclable // that is, storing recyclable to recycle ViewHolder before recyclable // If we can recycle Recycler before we can recycle it, notifyDataSetChanged calls onLayoutChildren // When RecyclerView fills items, we need to recycle Recycler before recycling. If there are ViewHolder recycled items, we need to recycle Recycler before recycling. detachAndScrapAttachedViews(recycler); . / / recycling, began to layout process / / here is to determine whether to reverse the layout, general to false if (mAnchorInfo. MLayoutFromEnd) {... } else {// Fill from anchor point up, and then from anchor point up, both of which are filled by calling fill method // 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; }}... }Copy the code
Recycler, a class that can handle level 4 caching, is also the focus of RecyclerView
Public final class Recycler {/ / # 1 mAttachedScrap mChangedScrap in l1, reuse you don't need to do bindViewHolder binding data ArrayList < ViewHolder > mAttachedScrap; ArrayList<ViewHolder> mChangedScrap; //#2 mCachedViews is a level 2 cache, where the ViewHolder corresponding to the item is stored as it slides up and down There is no need to re-bind data to the bindViewHolder when you slide into the screen again, you can adjust it using setItemCacheSize, ArrayList<ViewHolder> mCachedViews; ArrayList<ViewHolder> mCachedViews; MViewCacheExtension mViewCacheExtension mViewCacheExtension mViewCacheExtension // mRecyclerPool is used to cache mRecyclerPool when mCachedViews cannot be used to cache mRecyclerPool. // Can be adjusted by setRecycledViewPool, each type of capacity default 5 RecycledViewPool mRecyclerPool; }Copy the code
Process analysis of fill method
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { ... int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; / / open a loop, judge whether the current direction is the remaining space can fill the item while ((layoutState. MInfinite | | remainingSpace > 0) && layoutState. HasMore (state)) { . // Recycle layoutChunk(Recycler, State, layoutState, layoutChunkResult); // Recycle chunk (Recycler, State, layoutState, layoutChunkResult); . }... } // layoutChunk method void layoutChunk(RecyclerView.Recycler Recycler, RecyclerView.State State, LayoutState LayoutState, // Get views from recycler. This is the start of recycling. View View = layoutState.next(recycler); . if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == Layoutstate.layout_start)) {// The addView method of layoutManager is called to determine whether the viewholder corresponding to the view is to be deleted // If yes, add it to the recyclerView. If this view has been added to recyclerView before, do not need to add it again. Otherwise, add it to recyclerView. } else { addView(view, 0); } } else { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addDisappearingView(view); } else { addDisappearingView(view, 0); Margin measureChildWithMargins(View, 0, 0); margin measureChildWithMargins(View, 0, 0); . // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout When the position, we subtract margins. / / the View is put into the appropriate position, will consider ItemDecoration layoutDecoratedWithMargins (View, left, top, right, bottom); . }Copy the code
The recycling mechanism of Recycler
General summary:
- Whenever LayoutManager fills the list with items, it requests ViewHolder from Recycler
- When Recycler requests Recycler first looks for recyclable Viewholders from tier 1 caches, namely mAttachedScrap and mChangedScrap. These viewholders are in-screen and don’t need to be rebound
- If it cannot be found in the first-level cache, it will be found in the Second-level cache of mCacheViews. If it can be found, the position will be determined. If the position remains unchanged, it means that the item that just slipped out of the screen has been slid back, and there is no need to re-bind data; otherwise, for example, the item that slid up needs to use the reusable ViewHolder, in which case, it needs to be re-bound Data binding
- If the second level cache is not found, you need to look in the third level cache, which is developing this custom extension mViewCacheExt (rarely used).
- If there is a ViewHolder, the onCreateViewHolder of the Adapter class will be called and the ViewHolder will be returned
Above is the general overview of the reuse mechanism, and below is the code analysis
/ / the reuse stage, you will call Recycler# tryGetViewHolderForPositionByDeadline method ViewHolder tryGetViewHolderForPositionByDeadline (int position, boolean dryRun, long deadlineNs) { ... ViewHolder holder = null; // 0) If there is a changed scrap, ViewHolder if (mstate.ispreLayout ()) {holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! = null; } // 1) Find by position from scrap/hidden list/cache // If not, call childHelper#findHiddenNonRemovedView to see if there is a viewHolder that is valid for reuse from the collection being animated Haven't found it from mCachedViews lookup the if (holder = = null) {holder = getScrapOrHiddenOrCachedHolderForPosition (position, dryRun); if (holder ! = null) {// If the ViewHolder is not null // Whether the viewType of the ViewHolder is the same as the viewtype of the item to be filled // Whether there is a hasStableId unique identifier if (! validateViewHolderForOffsetPosition(holder)) { // recycle holder (and unscrap if relevant) since it can't be used if (! dryRun) { // we would like to recycle this but need to make sure it is not used by // animation logic etc. holder.addFlags(ViewHolder.FLAG_INVALID); if (holder.isScrap()) { removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); } holder = null; } else { fromScrapOrHiddenOrCache = true; } } } if (holder == null) { ... If (holder == null && mViewCacheExtension) {if (holder == null && mViewCacheExtension) {if (holder == null && mViewCacheExtension) ! = null) { // 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) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder" + exceptionLabel()); } else if (holder.shouldIgnore()) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view." + exceptionLabel()); } } } if (holder == null) { // fallback to pool if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + position + ") fetching from shared pool"); GetRecycledViewPool ().getRecycledView(type); if (holder ! = null) { holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { long start = getNanoTime(); if (deadlineNs ! = FOREVER_NS && ! mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { // abort - we have a deadline we can't meet return null; } / / really can't find in the cache, call createHolder method to develop a ViewHolder holder = mAdapter createViewHolder (RecyclerView. This type); if (ALLOW_THREAD_GAP_WORK) { // only bother finding nested RV if prefetching RecyclerView innerView = findNestedRecyclerView(holder.itemView); if (innerView ! = null) { holder.mNestedRecyclerView = new WeakReference<>(innerView); } } long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); }}}... }Copy the code
Here RecyclerView measurement, layout, reuse analysis to a paragraph.
Summary:
- RecyclerView is a plug – in design mode
- Try to specify the width and height of RecyclerView and Item; otherwise, dispatchLayoutStep2() may be called twice to measure in the process of onMeasure
- Try to use a directional refresh of notifyItemChanged instead of notifyDataSetChanged()