RecyclerView since its release, has been favored by developers, its good function decoupling, so that we become comfortable in the customization of its functions. Since using this control in my project, I’ve been so excited about it that I want to analyze it in a series of articles. This article starts with the most basic display analysis, for the analysis of the back to lay a solid foundation.
The basic use
This article first analyzes the RecyclerView from the creation to the display process, we first look at its basic use
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());
Copy the code
LayoutManager and Adapter are indispensable parts of RecyclerView. This article will analyze this code.
For convenience, in the later analysis, I’ll use RV for RecyclerView, LM for LayoutManager, and LLM for LinearLayoutManager.
The constructor
The View constructor is usually used to parse properties and initialize variables, and the RV constructor is no exception, and the relevant code for this article is as follows
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
// ...
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
// ...
});
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
// ...
});
// ...
}
Copy the code
You can roughly guess what these two classes do from their full names. AdapterHelper is an Adapter helper class that handles Adapter updates. ChildHelper is an RV helper class that manages its child views.
Sets the LayoutManager
public void setLayoutManager(@Nullable LayoutManager layout) {
// ...
/ / save the LM
mLayout = layout;
if(layout ! =null) {
LM saves the RV reference
mLayout.setRecyclerView(this);
// If the RV is added to the Window, notify LM
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this); }}// ...
// Request a relayout
requestLayout();
}
Copy the code
The main action of the setLayoutManager() method is that the RV and LM save references to each other, and since the RV’s LM has changed, the layout needs to be rerequested.
Set the Adapter
The setAdapter() method is implemented by setAdapterInternal()
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
// ...
// RV Saves Adapter references
mAdapter = adapter;
if(adapter ! =null) {
// Register the listener for the new Adapter
adapter.registerAdapterDataObserver(mObserver);
// Notify the new Adapter has been added to the RV
adapter.onAttachedToRecyclerView(this);
}
// If LM exists, notify LM that Adapter has changed
if(mLayout ! =null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
// Notify the RV that Adapter has changed
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
// Indicates that the Adapter has changed
mState.mStructureChanged = true;
}
Copy the code
The RV saves the Adapter reference and registers listeners for the new Adapter, and then notifies each listener concerned about the Adapter, such as RV, LM.
measurement
When everything is ready, it’s time to analyze the measurement
protected void onMeasure(int widthSpec, int heightSpec) {
// ...
// If LM uses automatic measurement mechanism
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// For compatibility processing, the RV's defaultOnMeasure() method is actually called
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// If both width and height are measured in EXACTLY mode, then use the default measurement and return it directly
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
/ /... Omit the rest of the measurement code
} else {
// ...}}Copy the code
First, the measurement logic is determined based on whether LM supports the RV’s automatic measurement mechanism. LLM supports automatic measurement mechanism, so only the measurement in this case is analyzed.
What is the automatic measurement mechanism, you can carefully read the source code comments and measurement logic, I only do a simple analysis here.
With automatic measurement, the LM onMeasure() is first called to take the measurement. Now, you might wonder, if it’s called automatic measurement, why do we measure it with LM. For compatibility purposes, it actually calls the RV’s defaultOnMeasure() method
void defaultOnMeasure(int widthSpec, int heightSpec) {
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);
}
Copy the code
We can see that RV, as a ViewGroup, saves the measurement results without considering the subview. Obviously, this is a rough measure.
But this rough measurement is actually designed to satisfy a special case where the parent View gives the MeasureSpec.EXACTLY. As you can see from the code, this particular case is handled after this step of rough measurement.
To simplify the analysis, only this special (and most common) case is considered for now. The omitted code is actually the code that considers the subview measurement, and this code is also found in onLayout(), so I’ll cover it later.
layout
OnLayout is implemented by dispatchLayout()
void dispatchLayout(a) {
// ...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
RV has been measured, so LM saves the RV measurement results
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if(mAdapterHelper.hasUpdates() || mLayout.getWidth() ! = getWidth() || mLayout.getHeight() ! = getHeight()) {// ...
} else {
// ...
}
dispatchLayoutStep3();
}
Copy the code
The layout process, however, is done with dispatchLayoutStep1(), dispatchLayoutStep2(), and dispatchLayoutStep3(). The only thing relevant to this article is dispatchLayoutStep2(), which is the actual layout operation that completes the child View, implemented by LM’s onLayoutChildren().
LM implements the layout of child views
From the previous analysis, we can see that the RV’s layout of the subviews is left to LM. LLM is used in the example, so its onLayoutChildren() method is analyzed here. Since this method is quite a lot of code, it will be parsed in steps.
Initialization information
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// Ensure that mLayoutState is created
ensureLayoutState();
mLayoutState.mRecycle = false;
// Resolve whether to use reverse layout
if(mOrientation == VERTICAL || ! isLayoutRTL()) { mShouldReverseLayout = mReverseLayout; }else {
mShouldReverseLayout = !mReverseLayout;
}
}
Copy the code
First, ensure that the LayoutState mLayoutState is created to hold the state of the layout.
In this example, LLM uses a vertical layout, and the layout uses a default that does not support RTL, so mShouldReverseLayout should be false, indicating that the layout is not inverted.
You need to know the reverse layout of the LLM.
Update anchor point information
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. Initialize information
// 2. Update anchor information
if(! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! =null) {
mAnchorInfo.reset();
// Anchor information is stored in reverse layout
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// Calculate the position and coordinates of the anchor points
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
// Indicates that the anchor information is valid
mAnchorInfo.mValid = true;
}
// ...
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;
}
// Notify the anchor information is ready
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
}
Copy the code
The AnchorInfo mAnchorInfo is used to hold the anchor information. The anchor location and coordinates indicate where the layout starts, as you’ll see later in the analysis.
MStackFromEnd (Boolean) is supported by the AbsListView#setStackFromBottom(Boolean) feature. In other words, it provides a consistent method of operation for developers. Personally, I think this is really a garbage operation.
After using updateAnchorInfoForLayout () method to calculate the position of the anchor and coordinates
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
// ...
// Determine the anchor coordinates based on the padding value
anchorInfo.assignCoordinateFromPadding();
// If not inverted, the anchor position is 0
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
// AnchorInfo#assignCoordinateFromPadding()
void assignCoordinateFromPadding(a) {
// If the layout is not reversed, the coordinate is the RV paddingTop value
mCoordinate = mLayoutFromEnd
? mOrientationHelper.getEndAfterPadding()
: mOrientationHelper.getStartAfterPadding();
}
Copy the code
Anchor#mPosition indicates the location where the data needs to be retrieved from the Adapter. Anchor#mCoordinate represents which coordinate point the subview needs to start filling the subview.
In the case of the example, the anchor point coordinate is the paddingTop of the RV, and the position is 0.
Calculate the extra space for the layout
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. Initialize information
// 2. Update anchor information
// 3. Calculate the extra space for the layout
// Save the layout direction. In the case of no scrolling, the value is layoutstate.layout_end
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// Calculate the extra space required for the layout and save the result to mReusableIntPair
calculateExtraLayoutSpace(state, mReusableIntPair);
// The padding should be considered
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
if(state.isPreLayout() && mPendingScrollPosition ! = RecyclerView.NO_POSITION && mPendingScrollPositionOffset ! = INVALID_OFFSET) {// ...}}Copy the code
At the time of the RV sliding, calculateExtraLayoutSpace () allocates a page of extra space, other cases will not allocate extra space.
For example, in the case of calculateExtraLayoutSpace () allocate extra space is 0. But for the layout, the extra space also takes into account the RV padding.
If a custom inherited from LLM LM, can copy calculateExtraLayoutSpace () defines the extra space allocation policy.
Layout for child views
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. Initialize information
// 2. Update anchor information
// 3. Calculate the extra space for the layout
// 4. Layout for child View
// First detach and recycle the subview
detachAndScrapAttachedViews(recycler);
// The RV height is 0, and the mode is UNSPECIFIED
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// Fill backward from the anchor position
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 forward from the anchor position
// ...
// If there is extra space, fill more subviews backwards
if (mLayoutState.mAvailable > 0) {
// ...}}Copy the code
Before laying out the child View, first separate the child View from the RV and reclaim it. Then, fill the subview backwards and forwards from the anchor position by filling (), and finally try to continue filling the subview backwards (if any) if there is room left.
The anchor position calculated from the example is 0 and the coordinate is paddongTop, so only the backfilling process from the anchor position is analyzed here.
First call updateLayoutStateToFillEnd () method, according to the anchor point information to update the mLayoutState
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
// The position and coordinates of the anchor are passed in as arguments
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
// Free space is the size available after the padding is removed
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
// Indicates the direction of data traversal for the Adapter
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
// Save the location of the data to be obtained from the Adapter
mLayoutState.mCurrentPosition = itemPosition;
// Save the orientation of the layout
mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
// Save the anchor coordinates, which are the offset of the layout
mLayoutState.mOffset = offset;
// Roll offset
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}
Copy the code
Once the mLayoutState information has been updated, fill() is called to fill the subview
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
// ...
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// There is free space, and there are subviews that are not filled
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// All child views have been displayed
if (layoutChunkResult.mFinished) {
break;
}
// Update the layout offset
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
// Recalculate the available space
if(! layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList ! =null| |! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; }// ...
}
// Return how much space is used in this layout
return start - layoutState.mAvailable;
}
Copy the code
As an example, if there is still free space and there are subviews that are not filled, then the layoutChunk() method will be called to fill the subview until the available space is exhausted or there are no subviews left.
LLM# layoutChunk () analysis
For the son View
LayoutChunk () is the core method that LLM uses to layout subviews. We need to focus on the implementation of this method. Since this method is also longer, I’m going to do it in sections as well
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
/ / 1. To obtain a child View with the update mLayoutState. MCurrentPosition
View view = layoutState.next(recycler);
}
Copy the code
RecyclerView. RecyclerView. RecyclerView
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// ...
if (holder == null) {
if (holder == null) {
/ / 1. Callback Adapter. OnCreateViewHoler create ViewHolder (), and set the ViewHolder type
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ...}}// ...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// ...
} else if(! holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 2. Call adapter.bindViewholder () to bind the ViewHolder and update some information about the ViewHolder
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 3. Make sure the layout parameters of the created View are correct and update the information
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;
}
// The layout parameter saves the ViewHolder
rvLayoutParams.mViewHolder = holder;
// If the View is not new and has already been bound, then mPendingInvalidate is true
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
Copy the code
First, create a ViewHolder object by calling the ViewHolder#createViewHolder() method
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
return holder;
}
Copy the code
Create a ViewHolder object with ViewHolder#onCreateViewHolder() and set the value mItemViewType to the ViewHolder object.
After the second step, create ViewHolder object, through ViewHolder# tryBindViewHolderByDeadline binding ViewHolder object () method
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
// Set the ViewHolder's mOwnerRecyclerView value to indicate that the ViewHolder is bound to the RV
holder.mOwnerRecyclerView = RecyclerView.this;
mAdapter.bindViewHolder(holder, offsetPosition);
/ / if in the pre - process layout, use ViewHolder. MPreLayoutPosition save ViewHolder location on the screen
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
return true;
}
public final void bindViewHolder(@NonNull VH holder, int position) {
// ViewHolder stores the location of the data in the Adapter
holder.mPosition = position;
// If each Item has a fixed ID, then the ViewHolder holds that ID
if (hasStableIds()) {
holder.mItemId = getItemId(position);
}
// ViewHolder sets the flag FLAG_BOUND
holder.setFlags(ViewHolder.FLAG_BOUND,
ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
/ / bind ViewHolder
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
holder.clearPayload();
final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
if (layoutParams instanceof RecyclerView.LayoutParams) {
// After binding, mInsetsDirty is set to true, indicating that its ItemDecoration needs to be updated
((LayoutParams) layoutParams).mInsetsDirty = true; }}Copy the code
The main thing is to bind a ViewHolder object via the ViewHolder#onBindViewHolder() method. In addition, we need to pay attention to the properties we set to the ViewHolder, some of which may be used when we analyze other procedures.
You need to know how to write a basic Adapter.
Third, after binding the ViewHolder object, you need to make sure that the View you created, ViewHolder#itemView, and its layout parameters are correct, and update some properties, such as the layout parameters that hold the bound ViewHolder object.
If you don’t know the layout parameters, refer to the ViewGroup implementation I wrote for LayoutParams.
Add the child View to the RV
Once you get the subview, you need to add it to the RV
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. Get the child View
// 2. Add the subview to the RV
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// Add the obtained subview to the end of the RV
addView(view);
} else{}}}Copy the code
The addView() method is implemented using base LM’s addViewInt(), which is ultimately implemented through McHildhelper.addview ().
// ChildHelper#addView()
void addView(View child, int index, boolean hidden) {
final int offset;
if (index < 0) {
// If index is -1, get how many child views the RV already has
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
// ...
// Add the subview to the RV based on the offset
mCallback.addView(child, offset);
}
Copy the code
Since the index value is -1, the View is added to the end of the RV’s child View.
Measuring the child View
After adding the subview to the RV, you need to measure the subview
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. Get the child View
// 2. Add the subview to the RV
// 3. Measure subview
measureChildWithMargins(view, 0.0);
// Save the size of LLM consumption in the corresponding direction
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
}
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Get Rect for ItemDecoration
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
// The Rect of ItemDecoration should be counted
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
// This is a measurement that takes into account the padding, margin, ItemDecoration Rect
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if(shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); }}Copy the code
For the subview measurement, consider the padding, margin, and Rect of ItemDecoration. How to measure it is beyond the scope of analysis in this paper.
Lay out the child View
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. Get the child View
// 2. Add the subview to the RV
// 3. Measure subview
// 4. Subview layout
// Calculate the coordinates required for the layout
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
} else{ top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; }}else {
// ...
}
// Do the layout
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
Copy the code
The layout process is very simple, but it’s a little overwritten here.
It is important to understand how custom views measure and lay out. This is the basis for your analysis of this article.
draw
The measurement and layout process has been analyzed, and all that remains is the drawing process
public void draw(Canvas c) {
// Call the onDraw() method
super.draw(c);
. / / ItemDecoration onDrawOver () to draw
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// Draw the boundary effect
// ...
}
public void onDraw(Canvas c) {
super.onDraw(c);
// Use ItemDecoration. OnDraw ()
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState); }}Copy the code
For the RV, drawing process is mainly drawing ItemDecoration, first is to use ItemDecoration. Ontouch () method for drawing, and then use ItemDecoration. OnDrawOver () to draw.
I’ll use a separate article in this series to explain how ItemDecoration works and how to use it.
feeling
RV source analysis really can not be accomplished overnight, requires great patience and perseverance. This article analyzes the RV process from creation to display in a minimalist (and still long) way, initially looking into the RV principle, but this is far from enough to satisfy my curiosity, and I will continue with this article as a building block.