The core logic of the previous mapping process is transferred to LayoutManager. In this article, we will analyze the source code of LinearLayoutManager in detail, analyze and complete the mapping process, and prepare for the implementation of LayoutManager in the future.
A couple of important ones.
1. generateDefaultLayoutParams()
LinearLayoutManager inherited RecyclerView LayoutManager, RecyclerView. LayoutManager is an abstract class. Is only an abstract method generateDefaultLayoutParams must implement.
public abstract LayoutParams generateDefaultLayoutParams(a);
Copy the code
This method sets the default LayoutParams for each item, and we must specify the implementation of this method. The implementation of the LinearLayoutManager is relatively simple. It’s adaptive in both directions.
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams(a) {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
Copy the code
2. onLayoutChildren
When analyzing the mapping process of RecycleView, the control mapping mainly calls the onLayoutChildren method of LayoutManager, which is an entrance and completes the measure and layout of each item internally to realize the filling of the first item. There are two parameters, the first is RecycleView reuse item recycling control Recycler, used for internal recycling to obtain view. The second is a centralized unit that holds all kinds of mapping information.
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
Copy the code
3. OnMeasure () and isAutoMeasureEnabled ()
As mentioned in onMeasure() and isAutoMeasureEnabled(), do not implement their logic at the same time. Overrides are mutually exclusive. IsAutoMeasureEnabled () is provided by the system for automatic mapping, onMeasure() if we rewrite our own measurement process. OnMeasure () should not be overridden if isAutoMeasureEnabled() returns true. If isAutoMeasureEnabled() is not overridden, it returns false by default, and onMeasure() should be overridden.
CanScrollHorizontally (), canScrollVertically(), scrollHorizontallyBy(), scrollVerticallyBy()
Slide series methods, the first two are whether the horizontal and vertical direction can slide. The last two are methods of processing during sliding. Only after the first two methods are released, which returns true, will RecycleView push touchEvent into LayoutManager. The latter two methods with sliding not only to achieve view recycling and reuse, but also for the new view fill, and scroll out of the screen recycling.
If we want to implement a LayoutManager ourselves (we will do so in a later section), we can override the above methods. So when analyzing the system LinearLayoutManager, we focus on the analysis of the above several methods, you can understand most of the functions.
The overall idea of LayoutManager running
- If isAutoMeasureEnabled() is not enabled, then LayoutManager#onMeasure() is used to measure. If it is enabled, the system dispatchLayoutStep method is used and we do not need to customize onMeasure().
- OnLayoutChildren is used to complete the first filling and notify refresh. Only each item under the display area will be filled by calculating the available area. The first screen is now complete.
- So we’re sliding RecycleView, which is a series of methods through canScrollHorizontally(), canScrollVertically(), scrollHorizontallyBy(), scrollVerticallyBy(), Configure reaction under slide. Complete dynamic filling, and invisible item recycling, new item reuse.
The division of labor within LayoutManager is relatively clear. OnLayoutChildren are responsible for filling the whole screen, while scrollHorizontallyBy() and scrollVerticallyBy() handle filling during sliding. We have both filling methods.
LayoutManager integrates sliding and measuring layout processing for RV. In fact, the bottom layer is the process of measuring the layout, because the sliding process also needs to be remeasured and rearranged.
This article only analyzes the full screen filling part of onLayoutChildren, and the next article will analyze the sliding part.
OnLayoutChildren Fills the entire screen
The precondition: state.ispreLayout () is false, which depends on whether the first measurement is completed and whether there is an animation. So the code with the condition true will be explained in the animation section.
OnLayoutChildren has a lot of code, but as a whole there are only two steps
- Confirm anchor information
- Measure the layout according to the anchor information
Step for confirming anchor information
// Process SavedState data
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;
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
if(! mAnchorInfo.mValid || mPendingScrollPosition ! = RecyclerView.NO_POSITION || mPendingSavedState ! =null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// Initializes the anchor information
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if(focused ! =null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
Copy the code
Let’s look at the points we need to analyze in turn
-
MPendingSavedState and mPendingScrollPosition mPendingSavedStates is cached data that can be restored to the previous state. Usually occurs during the process of destruction and reconstruction. The mPendingScrollPosition is set by scrollToPosition. That’s where the outside world wants to scroll to. The default is recyclerView.no_position.
-
ResolveShouldLayoutReverse () this method dealing with the direction of the layout processing, from the positive direction or in the opposite direction to fill. If the reading direction is horizontal and from right to left (such as Arabic style), the reverse direction is turned on by default. If mShouldReverseLayout is true, draw in the opposite direction, from the bottom or right. False is the opposite.
private void resolveShouldLayoutReverse(a) { // A == B is the same result, but we rather keep it readable if(mOrientation == VERTICAL || ! isLayoutRTL()) { mShouldReverseLayout = mReverseLayout; }else { // Horizontal and right-to-left orientation, reverse layout is enabled by default mShouldReverseLayout = !mReverseLayout; } } Copy the code
-
mLayoutFromEnd
First is mAnchorInfo mLayoutFromEnd Settings. This is true only if mShouldReverseLayout and mStackFromEnd are different. MStackFromEnd is set by setStackFromEnd and indicates whether the initial positioning starts at the end of the data, if true then displays from the last data. MAnchorInfo. MLayoutFromEnd is true, is the representation of the image is in the opposite direction to layout. Must enter the vertical direction, is the bottom of the screen to start layout.
-
UpdateAnchorInfoForLayout anchor configuration anchor configuration in updateAnchorInfoForLayout (recycler, state, mAnchorInfo) method. Take a look at AnchorInfo, the class that stores information about anchor points, for a look at the details inside.
class AnchorInfo { OrientationHelper mOrientationHelper; int mPosition; int mCoordinate; boolean mLayoutFromEnd; boolean mValid; } Copy the code
attribute meaning mOrientationHelper Is a help class, which has a lot of measurement layout help methods. mPosition Is the anchor of the anchor point, specifically the first position on the screen mCoordinate Represents the starting point of the drawing, and the initialization is the padding at the top and bottom mLayoutFromEnd Indicates whether to move from location to bottom of screen mValid Whether the anchor point is available UpdateAnchorInfoForLayout methods within specific setting anchor configuration was realized.
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) { if (updateAnchorFromPendingData(state, anchorInfo)) { return; } if (updateAnchorFromChildren(recycler, state, anchorInfo)) { return; } // Set the default starting point, that is, the top and bottom margins anchorInfo.assignCoordinateFromPadding(); // The default anchor position is the first data from the top and the last data from the bottom. anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; } void assignCoordinateFromPadding(a) { mCoordinate = mLayoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); } Copy the code
General structure: It can be seen that the anchor point is set through peding data first, and the data of mPendingScrollPosition is internally processed to the anchor point. Then set the anchor point through each children. None of them were successfully configured. Set the default anchor points.
- Anchor points are set through mPending data
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) { return false; } if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { // mPendingScrollPosition checks whether it is valid mPendingScrollPosition = RecyclerView.NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; return false; } anchorInfo.mPosition = mPendingScrollPosition; if(mPendingSavedState ! =null && mPendingSavedState.hasValidAnchor()) { // Process mPendingSavedState data anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; if (anchorInfo.mLayoutFromEnd) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingSavedState.mAnchorOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingSavedState.mAnchorOffset; } return true; } if (mPendingScrollPositionOffset == INVALID_OFFSET) { // Handle offset without setting it View child = findViewByPosition(mPendingScrollPosition); if(child ! =null) { final int childSize = mOrientationHelper.getDecoratedMeasurement(child); if (childSize > mOrientationHelper.getTotalSpace()) { // item does not fit. fix depending on layout direction anchorInfo.assignCoordinateFromPadding(); return true; } final int startGap = mOrientationHelper.getDecoratedStart(child) - mOrientationHelper.getStartAfterPadding(); if (startGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); anchorInfo.mLayoutFromEnd = false; return true; } final int endGap = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(child); if (endGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); anchorInfo.mLayoutFromEnd = true; return true; } anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper .getTotalSpaceChange()) : mOrientationHelper.getDecoratedStart(child); } else { // item is not visible. if (getChildCount() > 0) { // get position of any child, does not matter int pos = getPosition(getChildAt(0)); anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos == mShouldReverseLayout; } anchorInfo.assignCoordinateFromPadding(); } return true; } // override layout from end values for consistency anchorInfo.mLayoutFromEnd = mShouldReverseLayout; // if this changes, we should update prepareForDrop as well if (mShouldReverseLayout) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingScrollPositionOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingScrollPositionOffset; } return true; } Copy the code
The main processing method by scrollToPositionWithOffset (int position, int offset) method of the incoming data processing. That is, the processing point that specifies the scroll position and offset. The specific logic is quite clear, so I won’t go into details here.
- Anchor points are set with children data
Here, the existing childer is used to find the closest anchor point from the bottom or top according to the drawing. It is applied to notify series methods to refresh data. Because we already have children, we anchor the first view shown on the screen to save the current slide position. Our own LayoutManager, when the soft keyboard is up, if the anchor point is not set through the children data. Because RecyclerView redraws, it will go back to the beginning to refresh, not the current slide to the location.
- The default anchor
If neither step confirms the anchor point, the default anchor point is used.
anchorInfo.assignCoordinateFromPadding(); anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; Copy the code
The first row of RecycleView is used to create the padding property of the RecycleView. The second line sets the location of the anchor point, if setStackFromEnd sets the direction of the data display, and if it is at the bottom of the screen (true) the first screen displays the last data. If the top of the screen (false by default) displays the first data.
After the above three steps, the anchor point is determined. That’s the index that was drawn at the beginning. Next you need to confirm that the data is drawn and call fill to fill it.
Confirm drawing data
Confirm the drawing data mainly through updateLayoutStateToFillStart (mAnchorInfo) and updateLayoutStateToFillEnd (mAnchorInfo) two methods.
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; }}else {
// 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; }}if(! state.isPreLayout()) { mOrientationHelper.onLayoutComplete(); }else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
Copy the code
- By the method of detachAndScrapAttachedViews detach all the child. Detach removed all add word views and returned to the initial state of a wordless View, unifying the logic in the subsequent layout for the new layout. This section has to do with recycling, which will be covered in more detail in a later section on recycling reuse. Including the difference between Detach and remove.
- Determine the direction of the fill by mLayoutFromEnd, which is also mentioned above. He is from mAnchorInfo. MLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd Settings. If mLayoutFromEnd is true, fill from the end, vertical from the bottom, and horizontal from the right.
If from the end of the layout, first by updateLayoutStateToFillStart configuration, call the fill method the initial period of layout in the opposite direction, again by updateLayoutStateToFillEnd allocation to the terminal layout, the same end through the fill fill. Filling from the first paragraph is just the opposite.
Why is this design? Instead of going straight to the bottom
Let’s test this by contradiction, so if we’re filling from the end, we’re going to do it vertically, from the bottom of the screen.In this case, we need to determine the number of fillable Spaces from the top of the screen to determine our position, because the top is not enough to fill the remaining space, we draw directly from the bottom is wrong, will open the skylight.So from the above analysis we know why to design this way. Instead, the logic that starts at the top fills in the bottom first, because the first position must be at the very top of the screen. Fill in the lower side and then fill in the upper side, there’s no place to fill in, but if we go through scrollToPositionWithOffset
The offset () method is set to fill the upper part so that there is room to continue.
Call fill to fill
After confirming the anchor points and drawing the data, the main thing is to fill them with fill, which is an important method
fill(recycler, mLayoutState, state, false)
According to its parameters, Recycler is a unified control provided by RecycleView for recycling. MLayoutState, state contains various parameters for drawing, including starting point, direction, available space, etc. So this method can be used to structure the recycler view and fill it with parameters. Let’s look at the actual code.
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) {// If a slip occurs
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// Do the same
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
// Layout according to available space
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if(! layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList ! =null| |! state.isPreLayout()) {// New view layout to reduce available space
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
The logic is simple
- The first thing to do is to recycle, and this is the part where you recycle after sliding, which we’ll talk about in the next chapter.
- Calculate the remainingSpace, that is, the available layout space. The judgment inside the while is also to judge whether there is space to continue filling by remainingSpace. Consuming remainingSpace is done through the layoutChunkResult variable. LayoutChunkResult is assigned using the layoutChunk method, so the main layout logic is in layoutChunk, which is also an important method. Internally, the view measurement and layout of the specified postion is completed.
Let’s look at the specific layoutChunk code:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// Recycle through Recycler to get the next view to fill postion
View view = layoutState.next(recycler);
if (view == null) {
result.mFinished = true;
return;
}
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); }}/ / measurement
measureChildWithMargins(view, 0.0);
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; }}/ / layout
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
// 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
- First provides the next to get the next to the view of the operation, direct call here the recycler. GetViewForPosition method, internal encapsulates the reuse of logic, the following sections will be detailed explanation. MItemDirection is a little tricky here, because it means that the direction of the layout can only be 1 or -1.
View next(RecyclerView.Recycler recycler) {
if(mScrapList ! =null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
Copy the code
- After obtaining the view, we finally found the child view measurement layout, through measuring measureChildWithMargins, through layoutDecoratedWithMargins layout.
Step by step, until RecycleView fills up the available space, and then RecycleView completes the first screen.
conclusion
LayoutManager
The responsibilities mainly focus on the full screen layout filling, sliding layout filling, sliding processing, item measurement layout and other functions, we achieve our ownLayoutManager
Implement these functions as well- The logic of full screen filling first confirms anchor points, confirms layout parameters, and finally uses the data generated above to fill.
- Fill fill the size of the available area, that is, within the width and height of RecyclerView.
Analysis of LayoutManager source code. Combined with the surveying and mapping process of the first two chapters, we should have a deep understanding of the overall surveying and mapping from the table to the inside.
In the next chapter, we explain the sliding mechanism of RecycleView and analyze the processing of sliding and how to fill the logic of the layout.