android – Modifying vertical LinearLayoutManager so it can also be scrolled horizontally (video included)-ThrowExceptions

Exception or error:

I need to modify the vertical LinearLayoutManager which I use with a RecyclerView so it can be scrolled horizontally.

When the RecyclerView is scrolled vertically it adds new items because I’m extending LinearLayoutManager.

I thought this will solve the problem.

public class CustomLayoutManager extends LinearLayoutManager {


    public TableLayoutManager(Context context) {
        super(context);
    }

    @Override
    public boolean canScrollHorizontally() {
        return true;
    }

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int delta = -dx;

        offsetChildrenHorizontal(delta);

        return -delta;
    }

}

Now when I scroll the RecyclerView horizonatlly it works but the content can be scrolled offscreen.
Also when the content is scrolled horizontally and then I scroll vertically, the RecycerView will add the new views aligned with left screen border and not with initial views.

Video that demonstrates the behaviour:
http://youtu.be/FbuZ_jph7pw
What I’m doing wrong ?

UPDATE
(as suggested by @yigit)

Now I’m using the modified version of LinearLayoutManager (I’m not extending it anymore because I need to add offset to RecyclerView children in private method fill(….)).

These are the changes I made:

    private int childrenLeftOffset = 0;
    private int childrenMaxWith = 800;  // I must update this value with children max width
    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {

        int delta = -dx;
        int newOffset = childrenLeftOffset + delta;
        if (newOffset < 0) {
            if(newOffset < -childrenMaxWith) {
                //right edge reached
                offsetChildrenHorizontal(-(childrenMaxWith - Math.abs(childrenLeftOffset)));
                childrenLeftOffset  = -childrenMaxWith;
                delta = 0;  // RecyclerView will draw right edge effect.
            } else {
                //scroll horizontally
                childrenLeftOffset  = childrenLeftOffset + delta;
                offsetChildrenHorizontal(delta);
            }
        } else {
            //left edge reached
            offsetChildrenHorizontal(Math.abs(childrenLeftOffset));
            childrenLeftOffset  = 0;
            delta = 0; // RecyclerView will draw left edge effect.
        }

        return -delta;
    }

    private int fill(RecyclerView.Recycler recycler, RenderState renderState, RecyclerView.State state, boolean stopOnFocusable) {
            ........
            ........

            left = left + myLeftOffset;
            layoutDecorated(view, left + params.leftMargin, top + params.topMargin, right - params.rightMargin, bottom - params.bottomMargin);

            .......
            .......
    }

Scrolling CustomLinearLayout (updated video) http://youtu.be/DHeXkvpgruo

The problem now is to get the maximum width of RecyclerView children and update childrenMaxWith.

How to solve:

LinearLayoutManager does supports scrolling in one direction so when it needs to layout a new child, it simply starts from left.
offsetChildren method just moves children, does not keep any information.

You are better off cloning LinearLayoutManager and overriding layoutChunk method. But you’ll need to do it every time a new RV version is released.

Another possible (quick & dirty) solution is:

  • Keep track of your horizontal scroll position
  • override layoutDecorated in your LayoutManager and offset left and right values by your horizontal scroll offset, then call super.

This will probably work but has the potential of breaking in any future change in LayoutManger. It is not high risk though because that method is pretty core.

###

I recently had a similar problem with displaying tabular data in a RecyclerView. In my case, all of the views being recycled had the same width. I know that’s not the case with the original question, but going with a uniform width ensures that your columns are all aligned properly, so it’s easier to read that way. It also makes it much easier to determine when to stop scrolling horizontally; you just use getChildAt(0)?.measuredWidth. Also, the API might have changed since 2015 within LinearLayoutManager: I found that you have to override layoutDecoratedWithMargins instead of layoutDecorated if you want to use the “quick and dirty” solution suggested by yigit (it probably doesn’t hurt to override both). This is how I implemented it:

class MyRowLayoutManager(context: Context) : LinearLayoutManager(context, RecyclerView.VERTICAL, false) {

    private var horizontalOffset = 0

    override fun canScrollHorizontally(): Boolean = true

    override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
        if (childCount == 0) return 0

        val rowWidth = getChildAt(0)?.measuredWidth ?: return 0

        // if the row is narrower than the RecyclerView's width, don't scroll
        if (rowWidth <= width) return 0

        val remainingPxRight = rowWidth - width - horizontalOffset
        // cut scrolling short if we're about to reach end of the row
        val scrolled = when {
            dx > remainingPxRight -> remainingPxRight
            dx < -horizontalOffset -> -horizontalOffset
            else -> dx
        }

        horizontalOffset += scrolled
        offsetChildrenHorizontal(-scrolled)
        return scrolled
    }

    override fun layoutDecorated(child: View, left: Int, top: Int, right: Int, bottom: Int) {
        super.layoutDecorated(child, left - horizontalOffset, top, right - horizontalOffset, bottom)
    }

    override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
        super.layoutDecoratedWithMargins(child, left - horizontalOffset, top, right - horizontalOffset, bottom)
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *