An overview of the
Log RecyclerView source code learning process is helpful to consolidate their memory and deepen the understanding of the overall implementation mechanism.
In RecyclerView, the Adapter converts each item of the data source into each ViewHolder and monitors data changes. The ViewHolder holds the View as its name suggests, and uses the ViewHolder to bind the item data to the View it holds. RecyclerView delivers the layout arrangement of each item View to the subclass of LayoutManager for processing. In the layout process, Recycler can be used to cache and reuse ViewHolder to achieve optimization. RecyclerView also decouples the animation logic of item View to ItemAnimator.
Here from the measurement and layout process of RecyclerView to understand the internal implementation mechanism.
The source code to explore
In this paper, the source code is based on ‘androidx. Recyclerview: recyclerview: 1.1.0’
Measuring phase
RecyclerView.java
protected void onMeasure(int widthSpec, int heightSpec) {
// mLayout subclass of LayoutManager for developers
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
/ /...
}
Copy the code
First, it is judged that if LayoutManager is not set, the default measurement rule is adopted.
Look at the defaultOnMeasure method: [recyclerView.java]
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.
// chooseSize if SpecMode is EXACTLY, select SpecSize; If AT_MOST is set to
// Max (SpecSize, min(Padding and MinimumWidth)); Otherwise Max (Padding and MinimumWidth)
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
// Set the size of RecyclerView
setMeasuredDimension(width, height);
}
Copy the code
The default rule calculates the size based on the parent container’s SpecSize or RecyclerView Padding and MinimumWidth.
Back to onMeasure method: [recyclerView.java]
protected void onMeasure(int widthSpec, int heightSpec) {
/ /...
IsAutoMeasureEnabled returns false by default, however
/ / LayoutManager usually need to rewrite this method to return true, such as LinearLayoutManager, StaggeredGridLayoutManager.
if (mLayout.isAutoMeasureEnabled()) {
/ /...
} else {
/ /...
// defaultOnMeasure calls RecyclerView by default
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
/ /...}}Copy the code
Determines whether the custom LayoutManager overrides isAutoMeasureEnabled to return true. If false is returned, the RecyclerView default measurement rules are executed.
Use recyclerView.java to change isAutoMeasureEnabled to True.
protected void onMeasure(int widthSpec, int heightSpec) {
/ /...
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.
*/
// Set the size once according to the default rules (the size may not be accurate if the SpecMode is not EXACTLY and the data source is not empty)
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// When the specified size is specified or the data source is not set, the size set by the default rule is satisfied, and the measurement phase ends
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
// the initial state of mLayoutStep is STEP_START
if (mState.mLayoutStep == State.STEP_START) {
// Execute the first stage layout -- [1]
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
// Get size and mode from the Spec and assign them to the corresponding mWidth, mWidthMode, mHeight, and mHeightMode members.
// With Mode UNSPECIFIED, the mWidth and mHeight of the API version is set to 0 if the API version is less than 23.
mLayout.setMeasureSpecs(widthSpec, heightSpec);
// mIsMeasuring is used to indicate RecyclerView is currently calculating layout boundaries
mState.mIsMeasuring = true;
// Perform stage 2 layout -- [2] (In this stage, child is measured)
dispatchLayoutStep2();
// now we can get the width and height from the children.
// Iterate over child, calculate child layout boundaries (including separator lines), set RecyclerView size itself
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 a second measurement is required, shouldMeasureTwice returns false by default.
// Custom LayoutManager can override this method for its own layout features, returning true when both specModes are not EXACTLY wide and one child is not EXACTLY wide and height.
if (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 {
/ /...}}Copy the code
As can be seen, if AutoMeasure is enabled in RecyclerView measurement stage, part of the layout operation (dispatchLayoutStep1, dispatchLayoutStep2) will begin.
The layout is divided into three stages, corresponding to dispatchLayoutStep1, dispatchLayoutStep2 and dispatchLayoutStep3 respectively.
MState instance is State, used to store the current RecyclerView State useful information, such as the target scrolling position or view focus, can also retain any data identified by the resource ID. Its mLayoutStep is used to mark the current layout stage status, the initial status is STEP_START, execute dispatchLayoutStep1 method change to STEP_LAYOUT, Run dispatchLayoutStep2 and change to STEP_ANIMATIONS. Run dispatchLayoutStep3 and change back to STEP_START.
The layout phase
RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
// Send the layout process
dispatchLayout();
TraceCompat.endSection();
// mark the first layout complete
mFirstLayoutComplete = true;
}
Copy the code
dispatchLayout
DispatchLayout: recyclerView.java
void dispatchLayout(a) {
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;
// Check the layout status (dispatchLayoutStep1 and dispatchLayoutStep2 may be performed in onMeasure)
if (mState.mLayoutStep == State.STEP_START) {
// If no layout operation is performed during the measurement phase, it is executed from the beginning of the phase
dispatchLayoutStep1();
// EXACTLY MeasureSpec is generated and passed in with a call to setMeasureSpecs
mLayout.setExactMeasureSpecsFrom(this);
// Perform layout phase two
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.
// If there is a change in item or size, execute stage 2 again
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
// Perform layout phase three
dispatchLayoutStep3();
}
Copy the code
The Layout stage is called in the dispatchLayout method depending on state and change. Next, look at the three-stage approach to layout in turn.
dispatchLayoutStep1
RecyclerView is possible to execute dispatchLayoutStep1 and dispatchLayoutStep2 in onMeasure method. First look at dispatchLayoutStep1 method: [recyclerviet.java]
private void dispatchLayoutStep1(a) {
// Assertion checks the current state
mState.assertLayoutStep(State.STEP_START);
// Update the scroll offset saved in mState
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
/ / before requestLayout calls, to determine the layout of the redundant operations, and stopInterceptRequestLayout come in pairs
startInterceptRequestLayout();
// ViewInfoStore is used to save animation data
mViewInfoStore.clear();
// mLayoutOrScrollCounter is incremented by 1 to determine whether the layout is currently being evaluated and to avoid changing the adapter data source during layout and scrolling
onEnterLayoutOrScroll();
// Handle adapter data updates and set mState animation-related variables
processAdapterUpdatesAndSetAnimationFlags();
// Find the focus view and set the mState focus variable
saveFocusInfo();
// item animation related variable Settings
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
// Number of data
mState.mItemCount = mAdapter.getItemCount();
// Get the minimum and maximum ViewHolder layout index positions stored in the mMinMaxLayoutPositions array
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
// Record item animation data in ViewInfoStore.
MRunSimpleAnimations when mRunSimpleAnimations are true, it is possible to perform an item View fade in/fade out animation when the adapter data changes.
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
/ /...
}
// Record item animation data in ViewInfoStore.
When mRunPredictiveAnimations is true, the location of the old adapter index for each ViewHolder is saved and pre-laid out
if (mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
// Save old positions so that LayoutManager can run its mapping logic.
/ /...
// we don't process disappearing list because they may re-appear in post layout pass.
clearOldPositions();
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
/ / code called after layout of the trigger, and startInterceptRequestLayout come in pairs. When the input parameter is passed true,
// Layout operations are performed if delayed and unsuppressed layouts are marked.
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
Copy the code
This method mainly records the state before layout and index position information and other data.
dispatchLayoutStep2
DispatchLayoutStep2: recyclerView.java
private void dispatchLayoutStep2(a) {
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 actual layout operation is implemented by LayoutManager, which defaults to empty methods and needs to be overridden by subclasses.
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-checkmState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator ! =null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
Copy the code
This method does the actual child layout via the onLayoutChildren method of LayoutManager, which needs to be overridden by subclasses, For example, LinearLayoutManager and GridLayoutManager implement specific layout logic according to their arrangement characteristics.
dispatchLayoutStep3
DispatchLayoutStep3: recyclerview.java
private void dispatchLayoutStep3(a) {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
// Iterate to get the ViewHolder
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 there are ViewHolder changes (such as adding, removing, or changing positions), the item animation needs to be executed
if(oldChangeViewHolder ! =null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
// If an Item is CHANGED but the updated version is disappearing, it creates
// a conflicting case.
// Since a view that is marked as disappearing is likely to be going out of
// bounds, we run a change animation. Both views will be cleaned automatically
// once their animations finish.
// On the other hand, if it is the same view holder instance, we run a
// disappearing animation instead because we are not going to rebind the updated
// VH unless it is enforced by the layout manager.
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
// Add the animation information to the mLayoutHolderMap member of ViewInfoStore for saving
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 {
// Submit the animation task for executionanimateChange(oldChangeViewHolder, holder, preInfo, postInfo, oldDisappearing, newDisappearing); }}}else{ mViewInfoStore.addToPostLayout(holder, animationInfo); }}// Step 4: Process view info lists and trigger animations
// Iterate over the animation information in ViewInfoStore's mLayoutHolderMap collection and submit the animation task for execution
mViewInfoStore.process(mViewInfoProcessCallback);
}
// Clean up, recycle, reset and other operations
mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
if(mRecycler.mChangedScrap ! =null) {
mRecycler.mChangedScrap.clear();
}
if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
// Initial prefetch has expanded cache, so reset until next prefetch.
// This prevents initial prefetches from expanding the cache permanently.
mLayout.mPrefetchMaxCountObserved = 0;
mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
mRecycler.updateViewCacheSize();
}
// Notify LayoutManager to clean up when the complete layout process is complete
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1]) {// If the index position changes, the onScrollChanged and onScrolled callback methods are executed
dispatchOnScrolled(0.0);
}
// Retrieve the focus of the Item View
recoverFocusFromState();
resetFocusInfo();
}
Copy the code
This method is the last step of layout and mainly deals with item animation related and cache information cleaning and recycling.
LinearLayoutManager
The logic of RecyclerView layout is delegated to LayoutManager, developers can customize LayoutManager to achieve custom layout, and Android also provides several custom implementation, Such as LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager, here in LinearLayoutManager, for example, look at the layout to achieve process.
RecyclerView will call the onLayoutChildren method of LayoutManager in the layout stage, which is roughly divided into two steps:
- Update anchor index position, coordinate position and other information
- With anchor points as the benchmark to the top and bottom of the layout
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
/ /...
// In the onSaveInstanceState callback, the anchor information is saved in SavedState (inherited from Parcelable),
// Restore data assignment to mPendingSavedState in the onRestoreInstanceState callback.
// mPendingScrollPosition indicates the index position of the item data to scroll to.
if(mPendingSavedState ! =null|| mPendingScrollPosition ! = RecyclerView.NO_POSITION) {if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return; }}if(mPendingSavedState ! =null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
resolveShouldLayoutReverse();
// Get the focus view in the current ViewGroup
final View focused = getFocusedChild();
// The mAnchorInfo instance is AnchorInfo, which stores information such as the anchor index position and coordinate position
if(! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! =null) {
// If the current anchor information is invalid or there is a target position to be rolled or data to be recovered
mAnchorInfo.reset();
// Mark the arrangement direction from bottom to top or from top to bottom
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
// Update the anchor information in mAnchorInfo
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).
// Update anchor information with focus view position
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
/ /...
}
Copy the code
UpdateAnchorInfoForLayout method first looks up to scroll target location and data update anchor to restore information. If not, update anchor information with focus View. If not, use the item most visible near the top or bottom of the layout as the anchor point.
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
/ /...
// LLM may decide to layout items for "extra" pixels to account for scrolling target,
// caching or predictive animations.
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// When there is scrolling, calculate the extra layout space, top/left space is stored in mReusableIntPair[0], bottom/right space is stored in mReusableIntPair[1]
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
/ / mPendingScrollPositionOffset said item view starting on the edge of the edge and RecyclerView starting offset
/ / by scrollToPosition or scrollToPositionWithOffset make scroll to target index position, will be set mPendingScrollPosition and mPendingScrollPositionOffset
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
// Find the view to scroll to the target index position
final View existing = findViewByPosition(mPendingScrollPosition);
if(existing ! =null) {
final int current;
final int upcomingOffset;
// mShouldReverseLayout defaults to false
if (mShouldReverseLayout) {
current = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(existing);
upcomingOffset = current - mPendingScrollPositionOffset;
} else {
// Calculate the distance between the start (top/left) edge of the scrolling object view (including the partition line and margin) and the inner edge of the RecyclerView
current = mOrientationHelper.getDecoratedStart(existing)
- mOrientationHelper.getStartAfterPadding();
// Calculate the actual offset
upcomingOffset = mPendingScrollPositionOffset - current;
}
if (upcomingOffset > 0) {
extraForStart += upcomingOffset;
} else{ extraForEnd -= upcomingOffset; }}}/ /...
}
Copy the code
The main point here is that when there is a scroll to the specified position, the predictive layout item is used to smooth the scroll and calculate the allocation of extra space.
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
/ /...
int startOffset;
int endOffset;
final int firstLayoutDirection;
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
: LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
}
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// Recycler can temporarily recycle ViewHolder and View
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
// noRecycleSpace not needed: recycling doesn't happen in below's fill
// invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// Arrange items from the anchor point up and then from the anchor point down
// fill towards start
// Update mLayoutState information about the top space
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
// Layout item fills space
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
// Update the bottom space information in mLayoutState
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
// Layout item fills space
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
// Determine if there is any space left to populate the item
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; }}else {
// Arrange items from the anchor point down, then from the anchor point up
// fill towards end
// Update the bottom space information in mLayoutState
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
// Layout item fills space
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
// Update mLayoutState information about the top space
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
// Layout item fills space
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
// Determine if there is any space left to populate the item
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;
// Layout item fills space
fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; }}/ /...
}
Copy the code
This part calculates the distance between the anchor point and the space at the top and bottom of RecyclerView, and fills the layout of item from the anchor point to both ends. The core layout method is in fill method.
[LinearLayoutManager.java]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
/ /...
// changes may cause gaps on the UI, try to fix them.
// TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
// changed
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
// calculate gap,
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; }}// Execute the layout associated with the item animation
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if(! state.isPreLayout()) { mOrientationHelper.onLayoutComplete(); }else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
/ / the DEBUG...
}
Copy the code
Now look at the fill method:
[LinearLayoutManager.java]
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) {// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// Used to store layout execution results
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// As long as there is free space and item data, execute the layout continuously
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
// Layout a single View
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
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 -= 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; }}if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
Copy the code
In this method, the layoutChunk method is repeatedly called in a loop, one View at a time.
[LinearLayoutManager.java]
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// Obtain views through the Recycler's getViewForPosition method
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();
// mScrapList is null by default. MScrapList is set for item animation layout
if (layoutState.mScrapList == null) {
// Determine whether the layout is reversed when the space at the top of the anchor is arranged or not reversed when the space at the bottom of the anchor is arranged
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// Normally when you add a view to RecyclerView in the bottom space, it is gradually closer to the bottom boundary, and the view is added in the last child position
addView(view);
} else {
// Normally when you add a view to RecyclerView in the top space, it is gradually closer to the top edge, and the view adds the first child position
addView(view, 0); }}else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// Add a view to disappear, which will be removed after the animation is complete
addDisappearingView(view);
} else {
addDisappearingView(view, 0); }}// If the child calls requestLayout, RecyclerView disables the measurement cache, or allocates space to the child
// If the space is smaller than the previously measured space, call child.measure to measure again (the space occupied includes the delimiter and margin).
measureChildWithMargins(view, 0.0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
// Determine horizontal or vertical alignment
if (mOrientation == VERTICAL) {
// Calculate the top left, bottom right and bottom four boundaries assigned to the child
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.
// Call child.layout (space taken up includes delimiters and 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
// Check whether the item of the data set in the adapter corresponding to the view is deleted or changed
if (params.isItemRemoved() || params.isItemChanged()) {
// Mark mIgnoreConsumed to true so that the view consumes no space in the outer fill method
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
Copy the code
This method first creates a ViewHolder binding View through the View in the Recycler ViewHolder that retrieves the cache or through the Adapter.
After obtaining the View, add the View to RecyclerView via addView or addDisappearingView. The view added through addDisappearingView will also be removed after playing the disappearing animation if the item data in its corresponding adapter is deleted. AddView or addDisappearingView are finally called through callback to addView and attachViewToParent methods of ViewGroup.
After the view is added, child-measure will be called again to measure, and finally child-layout will be called to calculate the four boundaries for layout.
conclusion
RecyclerView layout core process is divided into three stages, corresponding to dispatchLayoutStep1, dispatchLayoutStep2, dispatchLayoutStep3 three methods, and in the measurement stage, the first two stages will be implemented in advance. Step2 RecyclerView will set its own size after the implementation. The first phase of the layout mainly records data such as the current state and the position of the ViewHolder index, as well as the item animation. The second stage does the actual layout, which is implemented by a subclass of LayoutManager. The third stage mainly carries out item animation related operations and cache data cleaning and recycling.