[toc]

LayoutManager

The cause is such a UI mei to the design:

At first, I had the idea of making a custom View, but later I found that this idea was somewhat immature, because I didn’t have the time and ability to complete such a control.

Fortunately, later found SpannedGridLayoutManager by the author Arasthel completed a row – and – column GridLayoutManager layout. The author’s ideas are worthy of reference.

After this period of development, I became curious about LayoutManager. How does it produce these effects? What steps do you need to take to implement something like LayoutManager?

GridLayoutManager

Because of the interest in this layout development, the first place to start is GridLayoutManager. If you look at the GridLayoutManager source code, you can see that it has three inner classes:

  • LayoutParams
  • SpanSizeLookup
  • DefaultSpanSizeLookup
LayoutParams

LayoutParams are familiar. It gives us the layout properties, the most basic width and height, the more detailed margin properties, and so on. Many container components have subclasses of their LayoutParams. Two new variables have been added to the GridLayoutManager’s LayoutParams: mSpanIndex and mSpanSize. What do they do? Let’s keep looking.

SpanSizeLookup

SpanSizeLookup is a helper class that provides the number of spans per ItemView. The default value is 1. It’s an abstract class, so look at what methods are defined inside:

Public Abstract static class SpanSizeLookup {/** * @param position * @return position Public abstract int getSpanSize(int position); Public int getSpanIndex(int position, int spanCount){}}Copy the code

In addition to getSpanIndex() and getSpanSize(), two of the more important methods, there are caching methods for caching the SpanSize of position.

The meaning and action of getSpanIndex() and getSpanSize() are better understood when combined with DefaultSpanSizeLookup.

DefaultSpanSizeLookup

DefaultSpanSizeLookup is the default implementation of SpanSizeLookup:

public static final class DefaultSpanSizeLookup extends SpanSizeLookup { @Override public int getSpanSize(int position) { return 1; } @Override public int getSpanIndex(int position, int spanCount) { return position % spanCount; }}Copy the code

You can see that getSpanSize() returns a default value of 1, and getSpanIndex() returns a value of the current position % total number of columns. If you think of a GridLayoutMananger as a grid with subscripts from 0 to SpanCount, a layout with SpanCount = 4 would have the following subscripts for each grid:

0,1,2,3, 0,1,2,3, 0,1,2,3...Copy the code

So if positionn = 0, spanIndex = 0, if positionn = 2, Positionn = 4 itemView spanIndex = 0.

To summarize, in each row, SpanIndex represents the beginning subscript of the span that the ItemView occupies, and spanSize represents how much span that ItemView occupies. If I set spanSize = spanCount:

val gridLayoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return 3
    }
}
Copy the code

You’ll get a LinearLayoutManager layout drawn using GridLayoutManager.

Layout of the implementation

Given the meaning of SpanSize and SpanIndex, it’s safe to assume that the GridLayoutManager uses them to determine the location of each ItemView in the container.

How to verify the truth of the guess? OnMeasure (), onLayout(), onDraw() Since this is a container component, the measurement and layout of the child View must be in onLayoutChildren(). Find the onLayoutChildren() method of the GridLayoutManager:

@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... super.onLayoutChildren(recycler, state); . }Copy the code

Again, inside it is the implementation of its parent class onLayoutChildren, which is the parent class: LinearLayoutManager.

Interject here, many predecessors have said, with the best problem to go to the source, my understanding is that if just want to understand a problem, so the best way is to quickly locate the specific code implementation, and not to focus too much now don’t need to understand the code, it will distract your experience and influence your thinking.

You can see there’s a lot of code in linearLayOutManager-onLayoutChildren (). I don’t know where to start to find out how subviews are measured and laid out. How do we locate the code that’s useful to us? Keywords subview, first have a subview and then can talk about the measurement and layout, in RecyclerView subview how to come? That’s onCreateViewHolder().

OnCreateViewHolder () onCreateViewHolder() onCreateViewHolder

. -> LinearLayoutManager.onLayoutChild() -> LinearLayoutManager.fill() -> LinearLayoutManager.layoutChunk() / GridLayoutManager.layoutChunk() -> LinearLayoutManager.LayoutState.next() -> RecyclerView.Recycler.getViewForPosition() -> RecyclerView.Recycler.tryGetViewHolderForPositionByDeadline() -> RecyclerView.Adapter.createViewHolder() -> onCreateViewHolder()Copy the code

It’s easy to locate the subview in the layoutChunk() method. It’s easy to locate the subview in the layoutChunk() method. It’s easy to locate the subview in the layoutChunk() method.

Before we start layoutChunk(), it’s helpful to know that LinearLayoutManager is the parent class of GridLayoutManager, And the GridLayoutManager overwrites layoutChunk(), which means that GridLayout is also drawn on a row, except that it divides each row into SpanCount columns and then draws an ItemView for each cell. This is the drawing logic of the GridLayoutManager. As shown in figure:

Let’s look at the implementation in layoutChunk() :

@Override void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { int count = 0; int consumedSpanCount = 0; int remainingSpan = mSpanCount; While (count < mSpanCount && LayoutState.hasmore (state) && LayoutSpan > 0) {int pos = layoutState.mCurrentPosition; final int spanSize = getSpanSize(recycler, state, pos); if (spanSize > mSpanCount) { throw new IllegalArgumentException("Item at position " + pos + " requires " + spanSize + " spans but GridLayoutManager has only " + mSpanCount + " spans."); } remainingSpan -= spanSize; if (remainingSpan < 0) { break; // item did not fit into this row or column } View view = layoutState.next(recycler); if (view == null) { break; } consumedSpanCount += spanSize; mSet[count] = view; count++; } if (count == 0) { result.mFinished = true; return; } // maxSize = 0; Float maxSizeInOther = 0; float maxSizeInOther = 0; // use a float to get size per span // we should assign spans before item decor offsets are calculated assignSpans(recycler, state, count, layingOutInPrimaryDirection); For (int I = 0; i < count; i++) { View view = mSet[i]; if (layoutState.mScrapList == null) { if (layingOutInPrimaryDirection) { addView(view); } else { addView(view, 0); } } else { if (layingOutInPrimaryDirection) { addDisappearingView(view); } else { addDisappearingView(view, 0); }} / / here is ItemDecorations interval between main computing itemview calculateItemDecorationsForChild (view, mDecorInsets); // measureChild View (View, otherDirSpecMode, false); / / get the View holds the width of the layout, including marigin + padding + width final int size = mOrientationHelper. GetDecoratedMeasurement (View); if (size > maxSize) { maxSize = size; } final LayoutParams lp = (LayoutParams) view.getLayoutParams(); final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) / lp.mSpanSize; if (otherSize > maxSizeInOther) { maxSizeInOther = otherSize; }} result.mConsumed = maxSize; Int left = 0, right = 0, top = 0, bottom = 0; if (mOrientation == VERTICAL) { if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { bottom = layoutState.mOffset; top = bottom - maxSize; } else { top = layoutState.mOffset; bottom = top + maxSize; } } else { if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { right = layoutState.mOffset; left = right - maxSize; } else { left = layoutState.mOffset; right = left + maxSize; } } for (int i = 0; i < count; i++) { View view = mSet[i]; LayoutParams params = (LayoutParams) view.getLayoutParams(); if (mOrientation == VERTICAL) { if (isLayoutRTL()) { right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex]; left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); } else { left = getPaddingLeft() + mCachedBorders[params.mSpanIndex]; right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); } } else { top = getPaddingTop() + mCachedBorders[params.mSpanIndex]; bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); } // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout The position, we subtract margins. / / layout of ziView 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) + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize); } // Consume the available space if the view is not removed OR changed if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } result.mFocusable |= view.hasFocusable(); } Arrays.fill(mSet, null); }Copy the code

Look at the implementation of the layoutChunk() method in three steps:

  • Gets all that can be placed in a rowThe child View
  • measurementThe child View
  • layoutThe child View

You can see that at the beginning of the layoutChunk() code a while loop is executed to get all the subviews that can be stored in the current line, which in plain English means “a layout with SpanCount = 4, four subviews with SpanSize = 1, Or two subviews of SpanSize = 2…. “

Next, we call measureChild() to measure the width and height of all the child views. We get the spacing, margin, and calculate the MeasureSpace value of the ziView width and height from these values.

GetSpaceForSpanRange () is a bit more confusing. It gets the width of an ItemView in the vertical direction. GetSpaceForSpanRange () as follows:

/** * @params startSpan ItemView takes the starting subscript of the cell * @params spanSize ItemView takes the total number of cells */ int getSpaceForSpanRange(int) startSpan, int spanSize) { if (mOrientation == VERTICAL && isLayoutRTL()) { return mCachedBorders[mSpanCount - startSpan] - mCachedBorders[mSpanCount - startSpan - spanSize]; } else { return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan]; }}Copy the code

Method uses an array called mCachedBorders, which is an array of size spanCount + 1, The array stores the X-axis coordinates of the spanCount + 1 line that divides the RecyclerView into spanCount columns. The first digit of the array is always 0.

MCachedBorders are calculated as follows:

static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) { if (cachedBorders == null || cachedBorders.length ! = spanCount + 1 || cachedBorders[cachedBorders.length - 1] ! = totalSpace) { cachedBorders = new int[spanCount + 1]; } cachedBorders[0] = 0; int sizePerSpan = totalSpace / spanCount; int sizePerSpanRemainder = totalSpace % spanCount; int consumedPixels = 0; int additionalSize = 0; for (int i = 1; i <= spanCount; i++) { int itemSize = sizePerSpan; additionalSize += sizePerSpanRemainder; if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) { itemSize += 1; additionalSize -= spanCount; } consumedPixels += itemSize; cachedBorders[i] = consumedPixels; } return cachedBorders; }Copy the code

For example, if recyclerView.width = 1080 and SpanCount = 3, then the mCachedBorder value is calculated as [0, 360, 720, 1080].

The RecyclerView is used to calculate the width of each ItemView by combining mCachedBorder with SpanSizeLookup.

So that’s the end of the measurement process, and you get the width and height of each itemView.

The next step is the layout phase, which is a little bit simpler here, where you get four values left, top, right, bottom, and an important one is layoutstate. mOffset which is the offset of the pixel that the row started with:

layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
Copy the code

LayoutChunkResult mConsumed is the maximum height of each block layout occupies, layoutState. MLayoutDirection layout filling direction:

  • LAYOUT_START: -1 Fill the layout from bottom to top
  • LAYOUT_END: 1 Fill the layout from top to bottom

If the GridLayoutManager is a vertical layout and fills the layout from top to bottom, the value of the layoutstate.moffset is the Y-axis coordinate at which the row was drawn from the start.

Since the width and height of the ItemView have been obtained in the previous measurement, why do we still use mCachedBorders when confirming the layout position? I wonder if this is based on those considerations.

That concludes the layout logic for the GridLayoutMananger. We also know that using the GridLayoutManager to create UI diagrams is not a good idea, because it only supports straddling columns, not lines. Fortunately, there is SpannedGridLayoutManager.

SpannedGridLayoutManager

Before explaining the implementation of SpannedGridLayoutManager, it is helpful to explain the implementation logic of SpannedGridLayoutManager.

The biggest difficulty in LayoutManager is to determine the layout property of the ItemView. In GridLayoutMananger, this is done by:

  • Layoutstate. mOffset: offset
  • SpanSize: indicates the SpanSize
  • SpanIndex: indicates the start index of the span
  • MCachedBorders: Border position
  • , etc.

To determine the layout property of the ItemView corresponding to a position.

How do the authors of SpannedGridLayoutManager solve this problem?

For example, to implement the above UI meimei to the design diagram (the following are in the vertical direction layout) :

First, the authors define the SpanSize class, which defines the number of rows and columns that an ItemView occupies in a cell:

/**
 * Helper to store width and height spans
 */
class SpanSize(val width: Int, val height: Int)
Copy the code

Then, a list of freeRects is defined to store all available rectangular ranges in the entire space occupied by RecyclerView:

The initial state of freeRects contains only one Rect, which has the value:

Rect(0, 0 - 3, 2147483647)
Copy the code

From the value of Rect, we can find that the author does not save the specific pixel distance value in Rect, but the number of cells. The area in the red box as shown in the figure represents the available range. Of course, the range at the bottom of the box is very large, which can occupy up to Int.MAX_VALUE cells.

Let’s put our first ItemView in two cells:

You can see that when the first ItemView is added to the graph, it divides the available ranges into two and sorts the two available ranges in a regular order. Then add a second ItemView whose width and height only occupy one cell, which can be added to the yellow box. Add and update the available range as follows:

Let’s add a third ItemView with the same width and height in the same cell, and add it to the yellow box:

At this time, you can find that the yellow box area is completely wrapped in the red box area, then you can remove the Rect representing the yellow box and only keep the red box area, as follows:

In this way, any ItemView can be set across rows and columns as long as it is available. Above is only the author’s basic idea, the specific code implementation logic is slightly different, the following is to see how to do it.

First look at the code structure, which contains two inner classes:

  • SpanSizeDefinition:itemviewNumber of rows and columns occupying a cell.
  • RectHelper: It implements the lookup, cache and update of the available rectangular area.

Can see SpannedGridLayoutManager and GridLayoutManager is different, it is integrated RecyclerView. LayoutMananger. Still use the GridLayoutManager method above to find the node closest to the answer.

protected open fun makeView(position: Int, direction: Direction, recycler: RecyclerView.Recycler): View {
    val view = recycler.getViewForPosition(position)
    measureChild(position, view)
    layoutChild(position, view)

    return view
}
Copy the code

It’s pretty clear, so you get itemView, measure, layout.

Let’s first look at the measure process:

protected open fun measureChild(position: Int, view: View) { val freeRectsHelper = this.rectsHelper val itemWidth = freeRectsHelper.itemSize val itemHeight = freeRectsHelper.itemSize val spanSize = spanSizeLookup? .getSpanSize(position) ? : SpanSize(1, 1) val usedSpan = if (orientation == Orientation.HORIZONTAL) spanSize.height else spanSize.width if (usedSpan > this.spans || usedSpan < 1) { throw InvalidSpanSizeException(errorSize = usedSpan, maxSpanSize = spans) } // This rect contains just the row and column number - i.e.: [0, 0, 1, 1] val rect = freeRectsHelper.findRect(position, spanSize) // Multiply the rect for item width and height to get positions val left = rect.left * itemWidth val right = rect.right * itemWidth val top = rect.top * itemHeight val bottom = rect.bottom * itemHeight val insetsRect = Rect() calculateItemDecorationsForChild(view, insetsRect) // Measure child val width = right - left - insetsRect.left - insetsRect.right val height = bottom - top - insetsRect.top - insetsRect.bottom val layoutParams = view.layoutParams layoutParams.width = width layoutParams.height =  height measureChildWithMargins(view, width, height) // Cache rect childFrames[position] = Rect(left, top, right, bottom) }Copy the code

So this part of the code is pretty easy to understand, first we get the SpanSize for position, and then we get the rectangle that fits our ItemView by recthelper.findRect (), Call RecyclerView. MeasureChildWithMargins () for measuring itemview. You can see in recthelper.findRect () that Rect is stored in the rectsCache array. A for loop is found in the onLayoutChildren() method, which retrives itemCount itemView layout areas and stores them in the rectsCache array.

Then there is the layout process:

protected open fun layoutChild(position: Int, view: View) { val frame = childFrames[position] if (frame ! = null) { val scroll = this.scroll val startPadding = getPaddingStartForOrientation() if (orientation == Orientation.VERTICAL) { layoutDecorated(view, frame.left + paddingLeft, frame.top - scroll + startPadding, frame.right + paddingLeft, frame.bottom - scroll + startPadding) } else { layoutDecorated(view, frame.left - scroll + startPadding, frame.top + paddingTop, frame.right - scroll + startPadding, frame.bottom + paddingTop) } } // A new child was layouted, layout edges change updateEdgesWithNewChild(view) }Copy the code

This part of the code is also pretty simple, but it adds scroll to it, and since this article doesn’t cover scroll, it’s pretty easy to understand without that.

In fact, the most important part of the entire SpannedGridLayoutManager is the acquisition of the ItemView layout area, most of which is found in the subtract() method. Understanding this approach also helps you understand what the author is doing with the layout.

Here is the end of the introduction, again sigh author’s train of thought, praise the spirit of open source. Read the GridLayoutManager and SpannedGridLayoutManager source code, found that RecyclerView for itemView layout is like playing a puzzle game feeling, from left to right, from top to bottom, To arrange each subview in sequence is similar to the processing logic we used to arrange the blocks from left to right and from top to bottom in similar jigsaw games. The authors of GridLayoutManager and SpannedGridLayoutManager have translated the logic of how our brains deal with similar problems into code.

Custom LayoutManager

Now let’s see what we need to do if we want to customize a LayoutManager, or if we need to override those methods.

class MyLayoutManager extends RecyclerView.LayoutManager { / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = must be rewritten / / / / * ~ only The abstract methods / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / * * * * Must override * it is the only 'abstract' method in 'LayoutManager' that provides LayoutParams for subclasses, * can use RecyclerView. LayoutParams () object can also be customized ViewGroup. LayoutParams object. * if the custom need to rewrite the following methods: * checkLayoutParams (LayoutParams) * generateLayoutParams (android. View. ViewGroup. LayoutParams) * GenerateLayoutParams (android. The content. The Context, Android. Util. AttributeSet) * / @ Override public RecyclerView. LayoutParams generateDefaultLayoutParams () {return null; } / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = must be rewritten / / / / * ~ is related to the layout / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / * * * * * main implementation must be rewritten */ @override public void onLayoutChildren(RecyclerView. Recyrecycler Recycler, RecyclerView RecyclerView.State state) { super.onLayoutChildren(recycler, state); } / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = must be rewritten / / / / * ~ sliding correlation / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / can * * * horizontal slip * / @Override public boolean canScrollHorizontally() { return super.canScrollHorizontally(); } / vertical sliding can * * * * / @ Override public Boolean canScrollVertically () {return. Super canScrollVertically (); } /** * Control the drift distance */ @override public int scrollHorizontallyBy(int dx, RecyclerView. RecyclerView. RecyclerView.State state) { return super.scrollHorizontallyBy(dx, recycler, state); } /** * Control the length of the loop */ @override public int scrollVerticallyBy(int dy, RecyclerView. RecyclerView. RecyclerView.State state) { return super.scrollVerticallyBy(dy, recycler, state); } /** * Override public void scrollToPosition(int position) {super.scrollToposition (position); } /** * Create a SmoothScroller instance and call startSmoothScroll() */ @override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { super.smoothScrollToPosition(recyclerView, state, position); } / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = / / ~ state related //============================================================================================== @Nullable @Override public Parcelable onSaveInstanceState() { return super.onSaveInstanceState(); } @Override public void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(state); }}Copy the code

If you want to support scrollbars you need to override these methods:

public int computeHorizontalScrollExtent(@NonNull State state) {
    return 0;
}

public int computeHorizontalScrollOffset(@NonNull State state) {
    return 0;
}

public int computeHorizontalScrollRange(@NonNull State state) {
    return 0;
}

public int computeVerticalScrollExtent(@NonNull State state) {
    return 0;
}

public int computeVerticalScrollOffset(@NonNull State state) {
    return 0;
}

public int computeVerticalScrollRange(@NonNull State state) {
    return 0;
}
Copy the code

So the most important thing is actually the onLayoutChildren() method, all the other methods can be written against the LinearLayoutManager or the GridLayoutManager, or just like the GridLayoutManager, It’s also possible to inherit from the LinearLayoutManager and concentrate on onLayoutChildren() to achieve the desired layout.

Leave a like if it helps you.