At the beginning, WHEN I was learning to customize LayoutManager, I searched for articles on the Internet, read blogs and some tweets from public accounts. When I first looked at it, I felt it was still the case, but in the deep LayoutManager source code slowly found that a lot of articles are actually unqualified, and even can be said to be very misleading, so I just want to write an article about custom LayoutManager, I hope I can help some people get started with custom LayoutManager.

Some excellent articles on custom LayoutManager are recommended

Although said in front of a lot of blog is not qualified, but there are also some excellent author’s blog inspired by the next great, especially Dave god building- A-Recyclerview-Layoutmanager -part of the series of articles, really can not speak great! Although this article is 14 years old, it is still the peak of custom LayoutManager related articles. Although this article is in English, it is still highly recommended to read!

Building a RecyclerView LayoutManager – Part 1

Building a RecyclerView LayoutManager – Part 2

Building a RecyclerView LayoutManager – Part 3

Accidentally found a station B big guy translated Dave explained the custom LayoutManager training video, this is a treasure, recommended collection for many times to watch.

Mastering RecyclerView Layouts

Secondly, Zhang Xutong published his blog about mastering custom LayoutManager in CSDN, especially the common mistakes and matters needing attention in the article. It is recommended to read it several times.

Blog.csdn.net/zxt0601/art…

For a valid LayoutManager, the childCount should not be greater than the number of items displayed on the screen, and the number of items in the scrapCache area should be zero.

The last is Chen Xiaoyuan’s Android custom LayoutManager eleventh type of flying Dragon in the sky, so big guy’s thinking is always so strange, logic is always so clear.

Blog.csdn.net/u011387817/…

Let’s start with a general approach to customizing LayoutManager

  1. inheritanceRecyclerView.LayoutManagerAnd implementgenerateDefaultLayoutParams()Methods.
  2. Rewrite as neededonMeasure()orisAutoMeasureEnabled()Methods.
  3. rewriteonLayoutChildren()Start populating the itemView for the first time.
  4. rewritecanScrollHorizontally()andcanScrollVertically()Method supports sliding.
  5. rewritescrollHorizontallyBy()andscrollVerticallyBy()Method populates and retrieves an itemView as it slides.
  6. rewritescrollToPosition()andsmoothScrollToPosition()Method support.
  7. Resolve the problem that the soft keyboard is ejected or folded uponLayoutChildren()A problem with methods being called again.

Let’s talk about the pitfalls of customizing layoutManagers

  1. useRecyclerViewOr,Inherited the LayoutManagerIt has its own reuse mechanism and view recycling
  2. Incorrectly overwrittenonMeasure()orisAutoMeasureEnabled()methods
  3. onLayoutChildren()The entire itemView is loaded directly
  4. Does not supportscrollToPosition()orsmoothScrollToPosition()methods
  5. Unresolved soft keyboard pop up or fold uponLayoutChildren()Method recall problem.

Use RecyclerView or inherit LayoutManager and you have your own reuse mechanism and view recycling, right?

I find that many people think RecyclerView is too perfect, and think that RecyclerView naturally has its own reuse mechanism and view recycling. As long as RecyclerView is used, it does not care about the number of items loaded. In fact, not very carefully read RecyclerView source can also find that RecyclerView is just a super ViewGroup that provides multi-level cache. And RecyclerView only completely delegates its onLayout method to LayoutManager, so inherited LayoutManager does not have its own reuse mechanism and view recycling.

LinearLayoutManager for example, there is a recycleByLayoutState() method in the LinearLayoutManager source code, which is called when scrolling to fill an itemView, to recycle an out-of-screen itemView, So when we customize LayoutManager, it’s up to us as developers to decide when to recycle the itemView!

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
            RecyclerView.State state) {...return scrollBy(dx, recycler, state);
    }    

    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {... fill(recycler, mLayoutState, state,false); . }int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) { recycleByLayoutState(recycler, layoutState); . layoutChunk() ... recycleByLayoutState(recycler, layoutState); }void recycleByLayoutState(a){...if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
        } else{ recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace); }}Copy the code

The onMeasure() or isAutoMeasureEnabled() method was incorrectly overridden

Override onMeasure() and isAutoMeasureEnabled() methods on demand. Because LayoutManger’s onMeasure() has a default implementation, and isAutoMeasureEnabled() returns false by default. This is why some blogs or Github source code have no problem with either method being overwritten, because they set width and height of RecyclerView to match_parent. Of course, you can do this if you are sure that your LayoutManager only supports width and height and requires match_parent to work properly.

So the question is when to override onMeasure(), when to override isAutoMeasureEnabled(), or are there cases where both methods can be overridden? Here’s my conclusion after reading a lot of source code and source comments: Don’t overwrite both methods at the same time, because they are mutually exclusive, as you can see from the source code. Overrides onMeasure() are also rare, unless, like my PickerLayoutManger, an absolute height is set to the LayoutManager. IsAutoMeasureEnabled () is a self-measuring mode for RecyclerView wrAP_content, which must be overridden if your LayoutManager supports WRAP_content.

OnLayoutChildren () directly loads all itemViews

There are several demos on blogs and Github, which are commonly written as follows:

 for (int i = 0; i < getItemCount(); i++) { View view = recycler.getViewForPosition(i); addView(view); . }Copy the code

Is it true that people who can write like this are not being funny? So if I’m onLayoutChildren I’m just going to iterate over itemCount and then addView, is that really killing me? OnCreateViewHolder = onCreateViewHolder = onCreateViewHolder = onCreateViewHolder = onCreateViewHolder = onCreateViewHolder When I have this idea, and also want to go to the comments section for advice, I found the above kind of writing method of variation, just stop, this troll is not worth 😏😏😏.

 for (int i = 0; i < getItemCount(); i++) { View view = recycler.getViewForPosition(i); addView(view); . Record the width, height, position and other information of some items..... recyler.recycleView(view) }Copy the code

The easiest way to test this is to set itemCount to in.max_value, and OK if no exception occurs.

The scrollToPosition() or smoothScrollToPosition() methods are not supported

Strictly speaking, this problem is not a big deal, but personally I think a qualified LayoutManager should adapt to these two methods. After all, RecyclerView’s scrollToPosition() and smoothScrollToPosition() is just the encapsulation of LayoutManager’s two methods, especially some open source libraries released to Github should be more suitable for these two methods.

An issue with soft keyboard popping up or recalling the onLayoutChildren() method is not resolved

This is a problem I find most people don’t notice, and there are some open source libraries that have this problem. The root of the problem is that LayoutManager calls back the onLayoutChildren() method when the EditText gets focus and the soft keyboard pops or collapses. If the onLayoutChildren method of a LayoutManager is not properly written, it will cause problems for users. The details will be explained below to start customizing LayoutManager.

LinearLayoutManager onLayoutChildren method has a piece of code that deals with this kind of problem, and is still an upgraded version.

 final View focused = getFocusedChild()
 ...
 else if(focused ! =null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
      mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
Copy the code

AssignFromViewAndKeepVisibleRect method is the key, interested can look at the source. The logic is: get the RecyclerView to get the focus of the itemView and its position, and start calculating its position to keep it visible on the soft keyboard.

Some useful apis

Before you start customizing LayoutManager, explain some Api usage so that you can get to the topic more quickly.

Get a View

 val view = recycler.getViewForPosition(position)
Copy the code

This method can retrieve a View from Recycler that is not null and can throw an exception if position is greater than itemCount or less than 0. The internal code logic is to retrieve a View from a different cache, return the View if it exists, create it with onCreateViewHolder if it does not exist and return it.

The Recycler class can simply be thought of as a recycling station that asks for views when it needs them or throws them when it doesn’t.

Add the View to RecyclerView

addDisappearingView(View child)
addDisappearingView(View child, int index)
  
addView(View child)
addView(View child, int index)
Copy the code

The addDisappearingView method is mainly used to support predictive animation, such as notifyItemRemoved animation

The addView method is more commonly used, and you’ll need it whenever you add a View.

Measure the View

measureChild(@NonNull View child, int widthUsed, int heightUsed)
measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed)
Copy the code

Both methods are used to measure the relevant information of the View. From the name, we can see that one method is calculated with margin, and the other method is not.

WidthUsed and heightUsed are also known by their names, so you can just pass 0, so the LinearLayoutManager will do.

* * note: ** Measurement View does not have to use these two methods, in special cases, you can also write your own measurement method, As in StaggeredGridLayoutManager is to rewrite the measurement measureChildWithDecorationsAndMargin (), And one of my open source libraries, PickerLayoutManager, uses the view.measure() native method directly in onMeasure.

Put the View

layoutDecorated(@NonNull View child, int left, int top, int right, int bottom)
layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
                int bottom) {
Copy the code

These two methods are nothing more than a wrapper around view.layout(), which should be familiar to anyone who has written a custom ViewGroup.

Get information about the View

int getPosition(@NonNull View view) 
Copy the code

Get the View’s Layout position. This is a very useful method that nobody has ever talked about.

int getDecoratedMeasuredWidth(@NonNull View child)
int getDecoratedMeasuredHeight(@NonNull View child)
Copy the code

Get the width and height of the View, and include the proportion of ItemDecoration.

int getDecoratedTop(@NonNull View child)
int getDecoratedLeft(@NonNull View child)
int getDecoratedRight(@NonNull View child)
int getDecoratedBottom(@NonNull View child)
Copy the code

Obtain the distance between the left, top, right and bottom of the View and the RecyclerView edge, and also include the proportion of ItemDecoration.

Move the View

offsetChildrenHorizontal(@Px int dx)
offsetChildrenVertical(@Px int dy)
Copy the code

Horizontal or vertical direction to move all child View, look at the source that is actually traversal call child View offsetTopAndBottom or offsetLeftAndRight method, these two methods in the custom ViewGroup to move child View is often used.

Recycling View

detachAndScrapAttachedViews(@NonNull Recycler recycler)
detachAndScrapView(@NonNull View child, @NonNull Recycler recycler)
detachAndScrapViewAt(int index, @NonNull Recycler recycler)
  
removeAndRecycleAllViews(@NonNull Recycler recycler)
removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler)
removeAndRecycleViewAt(int index, @NonNull Recycler recycler)
Copy the code

Just keep in mind that detachAndScrap starts with a lightweight recycled View, which will be added back soon. The onBindViewHolder method is executed when the View is added again.

I don’t see many blogs on the web that say exactly when and which methods to recycle a View. Here’s an easy way to tell which methods to recycle a View when:

  • Use the detachAndScrap series method in the onLayoutChildren Recycle View because onLayoutChildren will be called multiple times in a row, which is where the detachAndScrap series method is used.

  • The removeAndRecycle family of methods is used to recycle views that are not visible beyond the screen after scrolling has occurred.

Don’t ask me why we know, because I see LinearLayoutManager and StaggeredGridLayoutManager is so easy to use, hee hee! read the fucking source code~

OrientationHelper helper classes

OrientationHelper is an abstract class that abstracts a lot of convenient methods. Two static methods createHorizontalHelper and createVerticalHelper are provided to create help classes in the appropriate direction for developers to use. Using OrientationHelper can greatly reduce the boilerplate code I have in StackLayoutManager below.

    /** * move all child views */
    private fun offsetChildren(amount: Int) {
        if (orientation == HORIZONTAL) {
            offsetChildrenHorizontal(amount)
        } else {
            offsetChildrenVertical(amount)
        }
    }
...
    private fun getTotalSpace(a): Int {
        return if (orientation == HORIZONTAL) {
            width - paddingLeft - paddingRight
        } else {
            height - paddingTop - paddingBottom
        }
    }
Copy the code

Start customizing LayoutManager

Now we are going to explain how to customize a LayoutManager. We are going to use the PickerLayoutManager and StackLayoutManager libraries that I wrote. If you like, you can star.

Github.com/simplepeng/… Github.com/simplepeng/…

Inheritance LayoutManager and implement generateDefaultLayoutParams () method

That do not have what good say of, generateDefaultLayoutParams is abstract methods, inheritance LayoutManager must achieve, your custom LayoutManager itemView support what LayoutParams just write what, For example, THE PickerLayoutManager and StackLayoutManager I wrote are different implementations.

class PickerLayoutManager:: RecyclerView.LayoutManager() {override fun generateDefaultLayoutParams(a): RecyclerView.LayoutParams {
        return if (orientation == HORIZONTAL) {
            RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.MATCH_PARENT
            )
        } else {
            RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
            )
        }
    }
}
Copy the code
class StackLayoutManager: RecyclerView.LayoutManager() {override fun generateDefaultLayoutParams(a): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
    }
}
Copy the code

Override the onMeasure() or isAutoMeasureEnabled() method.

class PickerLayoutManager:: RecyclerView.LayoutManager() {override fun onMeasure(
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State,
        widthSpec: Int,
        heightSpec: Int
    ) {
        if (state.itemCount == 0) {
            super.onMeasure(recycler, state, widthSpec, heightSpec)
            return
        }
        if (state.isPreLayout) return

        // Assume that the width and height of each item are always the same, so use the first view to calculate the width and height,
        // This may not be a good idea
        val itemView = recycler.getViewForPosition(0)
        addView(itemView)
        // The measureChild method cannot be used. The measureChild method is not measureChild
// measureChildWithMargins(itemView, 0, 0)
        itemView.measure(widthSpec, heightSpec)
        mItemWidth = getDecoratedMeasuredWidth(itemView)
        mItemHeight = getDecoratedMeasuredHeight(itemView)
        // Recycle the View
        detachAndScrapView(itemView, recycler)

        // Set width and height
        setWidthAndHeight(mItemWidth, mItemHeight)
    }
  
      private fun setWidthAndHeight(
        width: Int,
        height: Int
    ) {
        if (orientation == HORIZONTAL) {
            setMeasuredDimension(width * visibleCount, height)
        } else {
            setMeasuredDimension(width, height * visibleCount)
        }
    }
}
Copy the code
class StackLayoutManager: RecyclerView.LayoutManager() {override fun isAutoMeasureEnabled(a): Boolean {
        return true}}Copy the code

PickerLayoutManager overrides onMeasure() and StackLayoutManager overrides isAutoMeasureEnabled().

Rewrite onLayoutChildren() to start filling the subview.

From this method on, the PickerLayoutManager and StackLayoutManager follow the same routine: Calculate the remaining space ->addView()->measureView()->layoutView(). Since the LinearLayoutManager is written to imitate the LinearLayoutManager, the following code examples will be used to compare the pseudo-code of StackLayoutManager.

Remember that most of the following is pseudocode and don’t copy it directly because StackLayoutManager supports many properties, including reverseLayout and orientation for LinearLayoutManager. And the following example will only talk about the orientation==HORIZONTAL code implementation, mainly for fear that the code logic is too complex to understand, want to see the specific source code can click the above source link to view.

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {

        // Lightweight remove the view from the screen
        detachAndScrapAttachedViews(recycler)
        // Start filling the view

        var totalSpace = width - paddingRight
        var currentPosition = 0

        var left = 0
        var top = 0
        var right = 0
        var bottom = 0
        // Imitate the LinearLayoutManager writing when the available distance is enough and to fill
        // Fill the View only when the position of the itemView is within the legal range
        while (totalSpace > 0 && currentPosition < state.itemCount) {
            val view = recycler.getViewForPosition(currentPosition)
            addView(view)
            measureChild(view, 0.0)

            right = left + getDecoratedMeasuredWidth(view)
            bottom = top + getDecoratedMeasuredHeight(view)
            layoutDecorated(view, left, top, right, bottom)

            currentPosition++
            left += getDecoratedMeasuredWidth(view)
          	/ / key point
            totalSpace -= getDecoratedMeasuredWidth(view)
        }
        // Output related information after layout is complete
        logChildCount("onLayoutChildren", recycler)
    }
Copy the code

The above code is very simple, I believe that people who have written a custom ViewGroup can understand. The above code simply implements a horizontal LinearLayoutManager, as shown in the figure below:

We added a method to print childCount after layout.

    private fun logChildCount(tag: String, recycler: RecyclerView.Recycler) {
        Log.d(tag, "childCount = $childCount -- scrapSize = ${recycler.scrapList.size}")}Copy the code

D/onLayoutChildren: childCount = 9 — scrapSize = 0 D/onLayoutChildren: childCount = 9 — scrapSize = 0 D/onLayoutChildren: childCount = 9 — scrapSize = 0

As you can see, we have a position0-8 itemView, so childCount=9 and scrapSize=0. Since we used totalSpace > 0 for the while expression, we don’t care how big the itemCount is.

Override canScrollHorizontally() and canScrollVertically() methods to support sliding.

Some itemView has been initialized above, but RecyclerView can not slide, do not believe you can try. We had to override the following two methods so that RecyclerView would pass sliding events to LayoutManager.

    override fun canScrollHorizontally(a): Boolean {
        return orientation == HORIZONTAL
    }

    override fun canScrollVertically(a): Boolean {
        return orientation == VERTICAL
    }
Copy the code

There’s nothing to say, return true if you want to support sliding in any direction. Return true at the same time, which supports sliding up, down, left, and right at the same time, similar to Dave’s tab-type LayoutManager.

Rewrite the scrollHorizontallyBy() and scrollVerticallyBy() methods to fill and reclaim the View as it slides.

override fun scrollHorizontallyBy(
    dx: Int,
    recycler: RecyclerView.Recycler,
    state: RecyclerView.State
): Int {
    return super.scrollHorizontallyBy(dx, recycler, state)
}
Copy the code

ScrollHorizontallyBy and scrollVerticallyBy

  • I saw some comments on some blogs that said, “You can’t swipe either way!” It is said that these two methods are sliding methods, but in fact, these two methods will only return the finger movement distance in RecyclerView to us, which is the corresponding methoddxanddy.dx>0Is your fingerSlide from right to left.dy>0Is your fingerSlide upBy the same token,dx,dy<0On the other hand, it’s up to the developer to actually move the View,LinearLayoutManagerIn simple useoffsetChildrenMethod to implement the movement. Or, “LayoutManager isn’t packaged properly enough, so we need to implement it ourselves!” I’m sure I’ve never seen a LayoutManager that can drag diagonally, or a LayoutManager that can transform an itemView while sliding.
  • two-methodThe return valueIt’s also important that RecyclerView knows how far LayoutManager is actually sliding,return 0RecyclerView will showoverScorllState andNestedScrollingThe subsequent processing of. aboutNestedScrollingI don’t see any blogs about that either. What? OverScorll you don’t know either! Say goodbye ~

Added the offsetChildrenHorizontal method to support horizontal sliding. What? Why is -dx again, look at the source code or experimental experiments will not know.

    override fun scrollHorizontallyBy(
        dx: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): Int {
        / / move the View
        offsetChildrenHorizontal(-dx)
        return dx
    }
Copy the code

As simple as that, our LayoutManager is ready to slide. But then there is a problem: “Sliding is only sliding among the existing children.” We didn’t write a method to fill or recycle a View. We certainly didn’t add a new itemView, and we didn’t recycle a View that went beyond the screen. Let’s start adding blocks to fill and recycle views.

    override fun scrollHorizontallyBy(
        dx: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): Int {

        / / fill the View
        fill(dx, recycler)
        / / move the View
        offsetChildrenHorizontal(-dx)
        / / recycling View
        recycle(dx, recycler)
      
        / / output children
        logChildCount("scrollHorizontallyBy", recycler)
        return dx
    }
Copy the code

As can be seen from the above code, we really only do three things when sliding: fill View, move View, recycle View. A qualified LayoutManager should do at least three things, and the sequence should be the same as the above code: first fill, then move, and finally recycle. Of course, the complex case of LayoutManager can add some more condition detection and special processing, for example, the LinearLayoutManager is first recycled, then filled, then recycled, and finally moved.

Let’s write the recycle method first, because the logic is relatively simple.

    private fun recycle(
        dx: Int,
        recycler: RecyclerView.Recycler
    ) {
        // To retrieve a collection of views, store it temporarily
        val recycleViews = hashSetOf<View>()

        //dx>0 is a finger that slides from right to left, so reclaim the children in front
        if (dx > 0) {
            for (i in 0 until childCount) {
                val child = getChildAt(i)!!
                val right = getDecoratedRight(child)
                //itemView right<0 means to recycle the View when it is out of the screen
                if (right > 0) break
                recycleViews.add(child)
            }
        }

        //dx<0
        if (dx < 0) {
            for (i in childCount - 1 downTo 0) {
                val child = getChildAt(i)!!
                val left = getDecoratedLeft(child)

                //itemView left> recyclerview. width
                if (left < width) break
                recycleViews.add(child)
            }
        }

        // Actually remove the View
        for (view in recycleViews) {
            removeAndRecycleView(view, recycler)
        }
        recycleViews.clear()
    }
Copy the code

As you can see, LayoutManager does recycle the out-of-screen itemView as we drag, and by looking at the log you can see that childCount and scrapSize are also valid.

D/scrollHorizontallyBy: childCount = 2 — scrapSize = 0

Next comes the important part, how to fill the View properly is a learning. LinearLayoutManager: LinearLayoutManager: LinearLayoutManager AddView, measureView, and layoutView as onLayoutChildren.

    private fun fill(dx: Int, recycler: RecyclerView.Recycler): Int {
        // Position to fill
        var fillPosition = RecyclerView.NO_POSITION
        // The available space, similar to totalSpace in onLayoutChildren
        var availableSpace = abs(dx)
        // Add an absolute value of sliding distance for easy calculation
        val absDelta = abs(dx)

        // The upper left and lower right of the View to be filled
        var left = 0
        var top = 0
        var right = 0
        var bottom = 0

        //dx>0 is the way to slide your finger from right to left, so fill the tail
        if (dx > 0) {
            val anchorView = getChildAt(childCount - 1)!!!!!val anchorPosition = getPosition(anchorView)
            val anchorRight = getDecoratedRight(anchorView)

            left = anchorRight
            // Fill the tail, then the next position should be +1
            fillPosition = anchorPosition + 1

            // If the position to be filled is beyond a reasonable range and the last View's
            // Right - distance to move < right edge (width
            if (fillPosition >= itemCount && anchorRight - absDelta < width) {
                val fixScrolled = anchorRight - width
                Log.d("scrollHorizontallyBy"."fill == $fixScrolled")
                return fixScrolled
            }

            // If the position of the trailing anchor minus dx is still out of screen, do not fill the next View
            if (anchorRight - absDelta > width) {
                return dx
            }
        }

        //dx<0 means fingers slide from left to right, so fill the head
        if (dx < 0) {
            val anchorView = getChildAt(0)!!!!!val anchorPosition = getPosition(anchorView)
            val anchorLeft = getDecoratedLeft(anchorView)

            right = anchorLeft
            // Fill the header, then the previous position should be -1
            fillPosition = anchorPosition - 1

            // If the position to be filled is beyond a reasonable range and the first View's
            //left+ moving distance > left edge (0
            if (fillPosition < 0 && anchorLeft + absDelta > 0) {
                return anchorLeft
            }

            // Do not fill the previous View if the head anchor position with dx is still out of the screen
            if (anchorLeft + absDelta < 0) {
                return dx
            }
        }

        // Keep filling in views according to the constraints
        while (availableSpace > 0 && (fillPosition in 0 until itemCount)) {
            val itemView = recycler.getViewForPosition(fillPosition)

            if (dx > 0) {
                addView(itemView)
            } else {
                addView(itemView, 0)
            }

            measureChild(itemView, 0.0)

            if (dx > 0) {
                right = left + getDecoratedMeasuredWidth(itemView)
            } else {
                left = right - getDecoratedMeasuredWidth(itemView)
            }

            bottom = top + getDecoratedMeasuredHeight(itemView)
            layoutDecorated(itemView, left, top, right, bottom)

            if (dx > 0) {
                left += getDecoratedMeasuredWidth(itemView)
                fillPosition++
            } else {
                right -= getDecoratedMeasuredWidth(itemView)
                fillPosition--
            }

            if (fillPosition in 0 until itemCount) {
                availableSpace -= getDecoratedMeasuredWidth(itemView)
            }
        }

        return dx
    }
Copy the code

The above code is deliberately wordy, so it should be easy to understand. And the smart baby should realize that the fill method is very coupled to the onLayoutChildren method and can actually be combined into one, just like the Fill method of the LinearLayoutManager. Again, remember that the code above is tutorial pseudocode, not the real StackLayoutManager code, I removed a lot of detection methods for ease of understanding, and it was very verbose.

D/scrollHorizontallyBy: childCount = 9 — scrapSize = 0 D/scrollHorizontallyBy: childCount = 10 — scrapSize = 0

Our LayoutManager now supports filling and recycling views while sliding, and childCount is still valid.

All that remains is boundary detection to support the overScrollMode. As you can see, the return value of fill has an Int, so now offsetChildren and scrollHorizontallyBy both use the return value of Fill.

    override fun scrollHorizontallyBy(
        dx: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): Int {

        // Fill in the View, so consumed is the fixed move value
        val consumed = fill(dx, recycler)
        / / move the View
        offsetChildrenHorizontal(-consumed)
        / / recycling View
        recycle(consumed, recycler)

        / / output children
        logChildCount("scrollHorizontallyBy", recycler)
        return consumed
    }
Copy the code

As simple as that, edge detection is done.

ScrollToPosition() and smoothScrollToPosition() methods are supported.

Adapter scrollToPosition ()

The LinearLayoutManager scrollToPosition() is a LinearLayoutManager.

    //LinearLayoutManager
		@Override
    public void scrollToPosition(int position) {
        mPendingScrollPosition = position;
        mPendingScrollPositionOffset = INVALID_OFFSET;
        if(mPendingSavedState ! =null) {
            mPendingSavedState.invalidateAnchor();
        }
        requestLayout();
    }
Copy the code

Was it that simple? Now let’s see what mPendingScrollPosition is.

    /** * When LayoutManager needs to scroll to a position, it sets this variable and requests a * layout which will check this variable and re-layout accordingly. */
    int mPendingScrollPosition = RecyclerView.NO_POSITION;
Copy the code

MPendingScrollPosition is the position to be scorll to, so let’s continue to find where it is called. After a series of searches, I found Hua Dian.

    private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
        if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        // validate scroll position
        if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
            mPendingScrollPosition = RecyclerView.NO_POSITION;
            mPendingScrollPositionOffset = INVALID_OFFSET;
            if (DEBUG) {
                Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition);
            }
            return false;
        }

        // if child is visible, try to make it a reference child and ensure it is fully visible.
        // if child is not visible, align it depending on its virtual position.anchorInfo.mPosition = mPendingScrollPosition; . }Copy the code

The updateAnchorFromPendingData () method have multilayer the call stack, but eventually the onLayoutChildren () method call. Remember we started with a variable currentPosition = 0 in onLayuoutChildren(), which is equivalent to the anchorinfo.mposition here, the position of the anchor point, Now we can conclude how to adapt scrollToPosition: Add the mPendingScrollPosition variable, assign it to the scrollToPosition() method, call requestLayout(), and then the onLayoutChildren() method will call back again, reassigning the anchor position, Remember to verify position.

 		private var mPendingPosition = RecyclerView.NO_POSITION

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State){... Omit codevar currentPosition = 0
        if(mPendingPosition ! = RecyclerView.NO_POSITION){ currentPosition = mPendingPosition } ... Omit code}override fun scrollToPosition(position: Int) {
        if (position < 0 || position >= itemCount) return
        mPendingPosition = position
        requestLayout()
    }
Copy the code

Take a look and see if our LayoutManager can scrollToPosition. But this is not the complete implementation, so if you compare the scrollToPosition LinearLayuotManager, you’ll see the difference. Complete implementation is mostly the details of the processing, and routine has nothing to do with understanding applause 👏👏👏👏.

One thing that I think most blogs don’t tell you is that onLayoutCompleted() is actually quite useful. Why didn’t anyone tell you? OnLayoutCompleted is called after onLayoutChildren() is called by LayoutManager and can be used to do a lot of finishing work. Example: Reset the value of mPendingScrollPosition

    //LinearLayoutManager
		@Override
    public void onLayoutCompleted(RecyclerView.State state) {
        super.onLayoutCompleted(state);
        mPendingSavedState = null; // we don't need this anymore
        mPendingScrollPosition = RecyclerView.NO_POSITION;
        mPendingScrollPositionOffset = INVALID_OFFSET;
        mAnchorInfo.reset();
    }
Copy the code
Adapter smoothScrollToPosition ()

Continue to LinearLayuotManager smoothScrollToPosition source code.

    //LinearLayuotManager
		@Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller linearSmoothScroller =
                new LinearSmoothScroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }
Copy the code

Ninety percent? Anyway, just copy and paste and see the effect, after all, the family CV engineer is not a wave of false reputation.

    override fun smoothScrollToPosition(
        recyclerView: RecyclerView,
        state: RecyclerView.State,
        position: Int
    ) {
        val linearSmoothScroller =
            LinearSmoothScroller(recyclerView.context)
        linearSmoothScroller.targetPosition = position
        startSmoothScroll(linearSmoothScroller)
    }
Copy the code

Yi! Isn’t that the effect of scrollToPosition? It’s not the smoothness of our smoothScroll. So I continue to look at my blog, and source code, or to see Dave a great god’s blog to find the real key computeScrollVectorForPosition (int targetPosition) this method. This method is just below the LinearLayoutManager smoothScrollToPosition method, but without comments, it is really hard to guess.

    @Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final intdirection = targetPosition < firstChildPos ! = mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction); }}Copy the code

This computeScrollVectorForPosition method is a way to SmoothScroller class. LinearSmoothScroller inherits from SmoothScroller.

        @Nullable
        public PointF computeScrollVectorForPosition(int targetPosition) {
            LayoutManager layoutManager = getLayoutManager();
            if (layoutManager instanceof ScrollVectorProvider) {
                return ((ScrollVectorProvider) layoutManager)
                        .computeScrollVectorForPosition(targetPosition);
            }
            Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
                    + " does not implement " + ScrollVectorProvider.class.getCanonicalName());
            return null;
        }
Copy the code

From the source view, and determine whether LayoutManager is a subclass of ScrollVectorProvider. If is is performing computeScrollVectorForPosition method, then this is something LinearLayoutManager sure ScrollVectorProvider interface is realized.

public class LinearLayoutManager extends RecyclerView.LayoutManager implements
        ItemTouchHelper.ViewDropHandler.RecyclerView.SmoothScroller.ScrollVectorProvider {
Copy the code

And as we expected, we went on to imitate it.

class BlogLayoutManager : RecyclerView.LayoutManager() ,RecyclerView.SmoothScroller.ScrollVectorProvider{
          override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
        if (childCount == 0) {
            return null
        }
        val firstChildPos = getPosition(getChildAt(0)!!val direction = if (targetPosition < firstChildPos) -1 else 1
        return PointF(direction.toFloat(), 0f)}}Copy the code

If you are careful, you will notice that we smoothly scroll to the position of 50, but 50 stops at the back instead of the front edge. Yes, that’s the correct effect, as is the effect of smoothScrollToPosition which includes the LinearLayoutManager. That’s why I said that scrollToPosition is not a complete effect, it should be the same as smoothScrollToPosition, scrollToPosition position should be filled from back to front, ScrollToPosition to the position in front is filled from front to back.

Then we talk about computeScrollVectorForPosition implementation routines in this method.

val firstChildPos = getPosition(getChildAt(0)!!val direction = if (targetPosition < firstChildPos) -1 else 1
return PointF(direction.toFloat(), 0f)
Copy the code

According to the source code comment, the key point is the return value of the PointF. The source code comment tells us that the size of the vector is not important, but the direction of the targetPosition and vector. The x of PointF represents the horizontal direction, and the Y represents the vertical direction. Integers represent forward movement and negative numbers represent reverse movement, which is the direction in the code above. However, this is not entirely true. If you need and can calculate the exact movement value, you can pass the exact value directly to PointF.

Fixed soft keyboard pop up or fold up onLayoutChildren() method to call again.

I came across this problem by accident.

As shown in the figure, after scrolling some distance, the soft keyboard pops up and LayoutManager automatically returns to Position =0. After scrolling some distance, the soft keyboard folds up and LayoutManager automatically returns to Position =0. The onLayoutChildren method is called again, because currentPosition=0 in onLayoutChildren method, so LayoutManager is rearranged from 0. Let’s begin by correcting position to the actual scrolling value.

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {

        var totalSpace = width - paddingRight

        var currentPosition = 0

        / / when childCount! If = 0, the View is already populated because of collection
        // Assign position to the first child
        if(childCount ! =0) {
            currentPosition = getPosition(getChildAt(0)!! }if(mPendingPosition ! = RecyclerView.NO_POSITION) { currentPosition = mPendingPosition }// Lightweight remove the view from the screen
        detachAndScrapAttachedViews(recycler)

        // Start filling the view
        var left = 0. Omit code}Copy the code

The sample code above note detachAndScrapAttachedViews (recycler) method is the modification of the position, because after the first call detachAndScrapAttachedViews, childCount would have been zero!

Again, we drag to our itemView with Position=25, and then the soft keyboard calls onLayoutChildren, and this time we do rearrange from currentPosition=25.

If we look at the image carefully, we can see that the Position =25 itemView is dragged out of the screen, but when we re-onLayoutChildren, the layoutView starts from the left edge of the screen. So what’s the solution? The LinearLayoutManager is a method that wraps sliding, filling, and recycling into a scrollBy() method. The LinearLayoutManager takes a fixOffset value and moves it around at the end of the relayout. Then at the end of the layout call scrollBy method to correct the offset. This will solve the problem of offset sliding while filling and recycling the View. I will use offsetChildren to correct the offset.

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {

        var totalSpace = width - paddingRight

        var currentPosition = 0
        var fixOffset = 0

        / / when childCount! If = 0, the View is already populated because of collection
        // Assign position to the first child
        if(childCount ! =0) {
            currentPosition = getPosition(getChildAt(0)!! fixOffset = getDecoratedLeft(getChildAt(0)!! }/ /... Omit code
        offsetChildrenHorizontal(fixOffset)
    }
Copy the code

OK~, call it a day! What? To implement a StackLayoutManager, why do you use this LinearLayoutManger! See here if you can still have this kind of problem, prove that I wrote a hydrology, escape ~

The pseudocode example above

The last

Learning to customize LayoutManager harvest a lot, especially some logical processing, heartfelt admiration of RecyclerView author, really all considered what situation. Although the daily use of RecyclerView of the LayoutManager is enough, but learn to custom LayoutManager can also as well, and in-depth can also deepen the understanding of RecyclerView, why not ~

It took me more than a month to learn to customize LayoutManager, to write several open source libraries, and then to complete this article. If you think this article is helpful, please give it a “like” or give a star to the open source library. Let me know that efforts will be rewarded, thank you ~

Github.com/simplepeng/…

Github.com/simplepeng/…