RecyclerView is one of the components we use a lot in Android development to display a lot of data. We should not only be proficient in using it, but also have a sense of its implementation. This article introduces the RecyclerView drawing process, that is, onMeasure, onLayout, onDraw these three methods mainly do what work, let’s go!
OnMeasure
We know that the method is to measure the width and height of the current View and child View, but look at the source code of RecyclerView found that the code is very long, it is not only done to measure the work, but also do some other work, let’s take a look
if (mLayout == null) {
// The first case
}
if (mLayout.isAutoMeasureEnabled()) {
// The second case is usually entered in this case
}else{
// The third case
}
Copy the code
The onMeasure method is divided into three cases based on conditional branching, which we will discuss one by one
- Case 1:
mLayout == null
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
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);
}
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
returnMath.max(desired, min); }}Copy the code
When mLayout == NULL, it can be seen from the incoming parameters that RecyclerView determines its size without considering the sub-view. It is a relatively rough measurement, and the specific size also needs to be determined according to the results of several subsequent measurements
- Case 2
mLayout.isAutoMeasureEnabled() == true
MLayout. IsAutoMeasureEnabled () is true, will be called LayoutManager. OnLayoutChildren (Recycler, State) to calculate the size of the View are needed, RecyclerView. The implementation class LayoutManager LinearLayoutManager, StaggeredGridLayoutManager overloading and the proposed method returns true, so usually goes into this branch (the listed part of the code)
if (mLayout.isAutoMeasureEnabled()) {
// Pass the measurement to LaytouManager
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// If width and height are already exact values, then the measurement is no longer required
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
// If a secondary measurement is required
if(mLayout.shouldMeasureTwice()) { dispatchLayoutStep2(); }}Copy the code
The first step calls the layoutManager.onMeasure () method
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
Copy the code
This method directly calls the default measurement method of RecyclerView, which is the first case we analyzed before. Reading the method’s comments, LayoutoManager strongly recommends turning on automatic measurement, and does not override the method if it is enabled, and the three default implementations of LayoutoManager do not override the method. The measurement strategy is also introduced. If the width and height measurement mode is UNSPECIFIED. AT_MOST, specify EXACTLY and RecyclerView occupies the maximum available space.
MeasureSpec.EXACTLY or if Adapter is not set.
STEP_START is the default value of mstate.mlayoutstep. Go to the conditional statement and execute dispatchLayoutStep1(), then dispatchLayoutStep2(), If a secondary measurement is required perform a dispatchLayoutStep2().
We focus on dispatchLayoutStep1() and dispatchLayoutStep2(). A variable closely related to these two methods is mstate.mlayoutstep. The significance of this variable is to determine dispatchLayoutStep1() and DISP AtchLayoutStep1 () and dispatchLayoutStep2() which step should be performed? It has three values
mLayoutStep | describe |
---|---|
STEP_START | Default value ordispatchLayoutStep3() It has been executed |
STEP_LAYOUT | dispatchLayoutStep1() It has been executed |
STEP_ANIMATIONS | dispatchLayoutStep2() It has been executed |
For the specific analysis of these three methods, we put onLayout processing, first say a conclusion dispatchLayoutStep1() to deal with the Adapter data update and prepare the animation before the data; DispatchLayoutStep2 () for itemView layout
-
Situation three: mLayout isAutoMeasureEnabled () = = false
We usually use layoutManagers that return true unless we customize them, so we won’t analyze this case for now
To summarize what onMeasure does, assume that the LayoutManager is a LinearLayoutManager
- Measure yourself. It may take several measurements
- If not both width and height
MeasureSpec.EXACTLY
Mode executes
dispatchLayoutStep1()
To deal withAdapter
Update and prepare the data before animationdispatchLayoutStep2()
Layout the itemView
OnLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
Copy the code
That’s dispatchLayout
void dispatchLayout(a) {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
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();
}
Copy the code
The process is clear
- if
Adapter
andLayoutManager
No layout without setting,RecyclerView
It’s just going to be blank - If before
onMeasure
To perform thedispatchLayoutStep1()
anddispatchLayoutStep2()
These two methods are no longer executed, howeverdispatchLayoutStep2()
It may need to be called again - perform
dispatchLayoutStep3()
Now what does the dispatchLayoutStep family of methods do
dispatchLayoutStep1()
/** * 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(a) {
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunSimpleAnimations){
/ /...
}
if (mState.mRunPredictiveAnimations){
/ /...
}
mState.mLayoutStep = State.STEP_LAYOUT;
}
Copy the code
DispatchLayoutStep1 () handles Adapter updates and the data needed to prepare the animation. While processAdapterUpdatesAndSetAnimationFlags () is used to handle Adapter updates and animation Flag processing, we take a look at this method
private void processAdapterUpdatesAndSetAnimationFlags(a) {
if (mDataSetHasChangedAfterLayout) {
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this); }} for the animation of the flag assignment mState. MRunSimpleAnimations =... mState.mRunPredictiveAnimations = ... }Copy the code
First deal with the renewal of the Adapter (Adapter. NotifyDataSetChanged () or RecyclerView. SwapAdapter (Adapter, Boolean) represent Adapter have update). It simply resets the operation of each item recorded previously. Because the data set changes, the information stored previously is meaningless. The following code assigns the flag bit of the animation. We call RecyclerView. SetAdapter and Adapter. NotifyDataSetChanged () will not trigger the animation, so we won’t consider animation related things.
Let’s move on to dispatchLayoutStep1(), and here are two if conditions, Involving two variables, mState. MRunSimpleAnimations and mState. MRunPredictiveAnimations only when the two variables in to perform the animation to true, so don’t consider the content inside. Mstate.mlayoutstep = state.step_layout, dispatchLayoutStep1() is done
Summary dispatchLayoutStep1() handles the data required for Adapter updates and preparing animations
- dispatchLayoutStep2()
private void dispatchLayoutStep2(a) {
mLayout.onLayoutChildren(mRecycler, mState);
mState.mLayoutStep = State.STEP_ANIMATIONS;
}
Copy the code
Method is very concise, calls the LayoutManager. OnLayoutChildren (Recycler Recycler, State State), the method is of the essence of the sub View layout method, but is an empty implementation, subclasses must implement this method, the statement is as follows
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, Statestate) ");
}
Copy the code
Mstate.mlayoutstep = state.step_animations Means dispatchLayoutStep2() has completed
Conclusion dispatchLayoutStep2 () call LayoutManager. OnLayoutChildren for the layout of the View
- dispatchLayoutStep3()
private void dispatchLayoutStep3(a) {
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
// Record the View information after the layout and trigger the animation
/ /...
}
/ /... Some cleaning up
}
Copy the code
Will first mState. MLayoutStep = State. STEP_START, mark dispatchLayoutStep3 () has been carried out, and then mState. MRunSimpleAnimations this variable indicates whether or not the animation, You don’t need animation for the first layout so you’re not going to go into this branch, and we’ll talk about animation later, and do some cleaning up at the end.
Summary dispatchLayoutStep3() trigger animation
Summarize the work done by onLayout, the general process is as follows
dispatchLayoutStep1()
The Adapter update and the data needed to prepare the animation were processeddispatchLayoutStep2()
Call LayoutManager. OnLayoutChildren for the layout of the ViewdispatchLayoutStep3()
Trigger the animation
draw
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
/ /... Handle clipToPadding="false"
}
Copy the code
onDraw
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState); }}Copy the code
Conclusion: draw and onDraw draw the RecyclerView dividing line, of course, dividing line needs us to achieve the specific drawing content, at the same time, we also know the difference between onDraw and onDrawOver in ItemDecoration
So far we have completed the source code analysis of RecyclerView three processes, most of the code listed above has been simplified, leaving out a lot of details, but just began to read the source code, as long as we grasp the overall process is good, put aside the details, the above overall process is not difficult to understand. But there is a very important method is not fine, is LayoutManager. OnLayoutChildren (), the method is the sub is the core of the View layout, we analysis on the method for a single wave to LinearLayoutManager (only consider the vertical) as an example to look over
LinearLayoutManager.onLayoutChildren()
We need to take a look at the helper classes that are used to help with the layout of subviews
- LayoutState
attribute | explain |
---|---|
mOffset |
How many pixels will be offset before the layout begins |
mAvailable |
The space available in the current layout direction |
mCurrentPosition |
You want to lay out where the child views are represented in the Adapter |
mInfinite |
There is no limit to the number of views a layout can have |
- AnchorInfo
attribute | explain |
---|---|
mPosition |
The anchor position |
mCoordinate |
Anchor point coordinate information |
mValid |
Whether the available |
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 a LayoutState
ensureLayoutState();
if(! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! =null) {
mAnchorInfo.reset();
// Update anchor information
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
/ /... Calculate the extra space required for the LinearLayoutManager
// The anchor information is ready
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// Cache all existing child views
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd){
/ /...
}else{
// fill towards end
// Save anchor information to mLayoutState
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
// fill towards start
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
if (mLayoutState.mAvailable > 0) {
fill(recycler, mLayoutState, state, false); }}}Copy the code
This code omits a lot of useful information, including assigning some useful attributes inside LayoutState. The comments at the beginning of the code provide insight into the internal execution logic of the method
- Locate anchor position and anchor coordinates
- Starting at the anchor point, populate the layout child View upward until the area is filled
- Starting at the anchor point, populate the layout subview downward until the area is filled
- Roll to meet requirements, such as stacking from the bottom
The key is the anchor point. For the LinearLayoutManager, it may not necessarily start the layout from the highest or lowest point, but it may start the layout from some point in the middle, as shown in the figure
RecyclerView
- Determine anchor point information
- The first layout, anchor information is definitely not available, into the update anchor conditional statement, inside the call
updateAnchorInfoForLayout(recycler, state, mAnchorInfo)
Update anchor information
- The first layout, anchor information is definitely not available, into the update anchor conditional statement, inside the call
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
// The first calculation method
if (updateAnchorFromPendingData(state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from pending information");
}
return;
}
// The second calculation method
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from existing children");
}
return;
}
if (DEBUG) {
Log.d(TAG, "deciding anchor info for fresh state");
}
// The third calculation method
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
Copy the code
You can see that there are three ways to calculate anchor information, each of which has a lot of code but is not difficult to understand
- Methods a
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
return false;
}
/ /...
}
Copy the code
MPendingScrollPosition == recyclerView. NO_POSITION Other values are available only when scrollToPosition is called or the mPendingScrollPosition recorded before onRestoreInstanceState is restored So by default this method does not calculate the anchor information, go down
- Method 2
updateAnchorFromChildren
This method determines anchor information based on the subview- If there are no child views, return directly
false
, indicates that the anchor point information is not calculated - If there is a child View, the anchor is usually the position of the child View visible on the screen. I’m going to take the first visible View on the screen, the child View of Positon =1,
anchorInfo.mCoordinate
The Decor top location assigned to subview 1
- If there are no child views, return directly
The method of detailed analysis can see this article RecyclerView source code analysis
- Methods three
MCoordinate = 0 if the padding is not set, Anchorinfo. mPosition = 0(mStackFromEnd == false, which defaults to false)
Anchor point information is calculated mainly to assign values to the two variables mPosition and mCoordinate, so that we know which point to start filling the sub-view and the position of the data corresponding to the sub-view in the Adapter
After updating the anchor point information, there is a long section of code in the source code to calculate the “extra space” required by the LinearLayoutManager. This code also does not understand, so I will not analyze it, but it does not affect our grasp of the overall layout process. Anchor point information is ready, updateLayoutStateToFillEnd () to save anchor point information to the mLayoutState, then call the fill () method to fill the child View, mAnchorInfo. MLayoutFromEnd filling can be divided into two cases
true
From:Adapter
Let’s start with the last one and go from the bottom upfalse
From:Adapter
Starting with the first item, the layout from top to bottom (the default) looks like this (dotted lines indicate the ones outside the screenItemView
)
The default is false, starting the layout from the top down and then entering the key fill() method
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// Free space
final int start = layoutState.mAvailable;
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
// Save the space consumed by each child View
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// Loop layout subviews
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// Core method
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if(! layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList ! =null| |! state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed;// we keep a separate remaining space because mAvailable is important for recycling
// Consume the space occupied by the child View
remainingSpace -= layoutChunkResult.mConsumed;
}
/ /...
}
return start - layoutState.mAvailable;
}
Copy the code
The core idea of fill is to continuously arrange subviews in a loop. LayoutChunk is responsible for filling when there is no available space or data from the data source. For each subview, the remaining space is subdivided by the space occupied by the corresponding View (vertically, that is, the height), and then the next one is filled. Finally, the size of the area filled in the layout is returned.
Let’s go into layoutChunk to see the implementation in action
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// Get a child View
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;
}
// Add the View to RecyclerView
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0); }}else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0); }}// Measure the size of sub-views including the margins and decorations
measureChildWithMargins(view, 0.0);
// Save the occupied space in LayoutChunkResult for the outer layer to recycle
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else{ top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; }}else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else{ left = layoutState.mOffset; right = layoutState.mOffset + result.mConsumed; }}// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
// Put the child View in the appropriate position
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
Copy the code
- The first step
View view = layoutState.next(recycler)
To get aitemView
And here it isRecyclerView
Caching mechanisms are discussed in a later chapter. - The second step is to go
itemView
Added to theRecyclerView
, divided into two casesaddView
It makes sense to call the method after making some logical judgments about correct placement, avoiding repeated additions, and so onViewGroup
theaddView
To implement.addDisappearingView
Represents that the View is about to disappear from the screen, such as an underline or callAdapter.notifyItemRemoved
, the method and the aboveaddView
All calls are internaladdViewInt(View child, int index, boolean disappearing)
, but the last parameter is different.
- Step 3 Measurement
itemView
The size of themeasureChildWithMargins(view, 0, 0)
Inside this method, there are other considerations besides its own sizemargin
anddecorations
The size of a dividing line. After measuring, save the consumed space toLayoutChunkResult
For the outer layer to recycle. - The fourth step will
itemView
Put it in the right place, when you calculate the positionlayoutState.mOffset
It depends on the coordinates of the anchor points that we calculated earlier, if it’s the first oneitemView
,layoutState.mOffset
The coordinates are the same as the anchor points, and you can debug to see the data correspondence. Of course, the layout was also consideredmargin
anddecorations
(What we call a dividing line)
Above will fill () method of analysis is completed, LinearLayoutManager. OnLayoutChildren core is analyzed, and finally a layoutForPredictiveAnimations, from the perspective of the annotation of the method, It is for the purpose of animation to do some layout, and it is not necessary to execute, so I will not analyze it, if there are readers who know this content, I hope you can let me know.
. At this point, LinearLayoutManager onLayoutChildren analysis is completed, but the method annotation of the last article, posted on the original 4) scroll to fulfill requirements like stack from bottom. I don’t see where it is reflected, maybe it is included in some details omitted above. In short, I don’t understand this point, if there are readers who are clear, I hope you can tell me.
conclusion
RecyclerView drawing process we finished the analysis, summarize
onMeasure
withLayoutManager
Whether or not automatic measurement is enabled is dependent, if automatic measurement is supported, it may be pre-layout, the default implementation of threeLayoutManager
All support automatic measurement, if customLayoutManager
Pay attention to thatonLayout
Mainly in thedispatchLayoutStep1()
,dispatchLayoutStep1()
,dispatchLayoutStep1()
These three methods are called sequentially, with the first and third dealing mainly with animation, and the second handing over layout tasksLayoutManager
draw
andonDraw
Call theItemDecoration
We implement these methods from the defined splitter line
Finally, due to the author’s limited level, if there is any mistake in the above analysis, welcome to put forward, I will correct it in time, so as not to mislead others
The relevant data
RecyclerView source code analysis (a) – RecyclerView three processes
RecyclerView source code analysis
RecyclerView analysis