android – How do I create a circular (endless) RecyclerView?-ThrowExceptions

Exception or error:

I am trying to make my RecyclerView loop back to the start of my list.

I have searched all over the internet and have managed to detect when I have reached the end of my list, however I am unsure where to proceed from here.

This is what I am currently using to detect the end of the list (found here):

 @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

        visibleItemCount = mLayoutManager.getChildCount();
        totalItemCount = mLayoutManager.getItemCount();
        pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();

        if (loading) {
            if ( (visibleItemCount+pastVisiblesItems) >= totalItemCount) {
                loading = false;
                Log.v("...", ""+visibleItemCount);
            }
        }
 }

When scrolled to the end, I would like to views to be visible while the displaying data from the top of the list or when scrolled to the top of the list I would display data from the bottom of the list.

For example:

View1 View2 View3 View4 View5

View5 View1 View2 View3 View4

How to solve:

There is no way of making it infinite, but there is a way to make it look like infinite.

  1. in your adapter override getCount() to return something big like Integer.MAX_VALUE:

    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }
    
  2. in getItem() and getView() modulo divide (%) position by real item number:

    @Override
    public Fragment getItem(int position) {
        int positionInList = position % fragmentList.size();
        return fragmentList.get(positionInList);
    }
    
  3. at the end, set current item to something in the middle (or else, it would be endless only in downward direction).

    // scroll to middle item
    recyclerView.getLayoutManager().scrollToPosition(Integer.MAX_VALUE / 2);
    

###

The other solutions i found for this problem work well enough, but i think there might be some memory issues returning Integer.MAX_VALUE in getCount() method of recycler view.

To fix this, override getItemCount() method as below :

@Override
public int getItemCount() {
    return itemList == null ? 0 : itemList.size() * 2;
}

Now wherever you are using the position to get the item from the list, use below

position % itemList.size()

Now add scrollListener to your recycler view

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 
        super.onScrolled(recyclerView, dx, dy);
        int firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition();
        if (firstItemVisible != 0 && firstItemVisible % itemList.size() == 0) {
            recyclerView.getLayoutManager().scrollToPosition(0);
        }
    }
});

Finally to start auto scrolling, call the method below

public void autoScroll() {
    final Handler handler = new Handler();
    final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            recyclerView.scrollBy(2, 0);
            handler.postDelayed(this, 0);
        }
    };
    handler.postDelayed(runnable, 0);
}

###

In addition to solution above.
For endless recycler view in both sides you should add something like that:

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            val firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition()
            if (firstItemVisible != 1 && firstItemVisible % songs.size == 1) {
                linearLayoutManager.scrollToPosition(1)
            }
            val firstCompletelyItemVisible = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
            if (firstCompletelyItemVisible == 0) {
                linearLayoutManager.scrollToPositionWithOffset(songs.size, 0)
            }
        }
    })

And upgrade your getItemCount() method:

@Override
public int getItemCount() {
        return itemList == null ? 0 : itemList.size() * 2 + 1;
}

It is work like unlimited down-scrolling, but in both directions. Glad to help!

###

Amended @afanit’s solution to prevent the infinite scroll from momentarily halting when scrolling in the reverse direction (due to waiting for the 0th item to become completely visible, which allows the scrollable content to run out before scrollToPosition() is called):

val firstItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstItemPosition != 1 && firstItemPosition % items.size == 1) {
    layoutManager.scrollToPosition(1)
} else if (firstItemPosition == 0) {
    layoutManager.scrollToPositionWithOffset(items.size, -recyclerView.computeHorizontalScrollOffset())
}

Note the use of computeHorizontalScrollOffset() because my layout manager is horizontal.

Also, I found that the minimum return value from getItemCount() for this solution to work is items.size + 3. Items with position larger than this are never reached.

###

I was running into OOM issues with Glide and other APIs and created this Implementation using the Duplicate End Caps inspired by this post for an iOS build.

Might look intimidating but its literally just copying the RecyclerView class and updating two methods in your RecyclerView Adapter. All it is doing is that once it hits the end caps, it does a quick no-animation transition to either ends of the adapter’s ViewHolders to allow continuous cycling transitions.

http://iosdevelopertips.com/user-interface/creating-circular-and-infinite-uiscrollviews.html

class CyclingRecyclerView(
    context: Context,
    attrs: AttributeSet?
) : RecyclerView(context, attrs) {
    // --------------------- Instance Variables ------------------------
    private val onScrollListener = object : RecyclerView.OnScrollListener() {

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            // The total number of items in our RecyclerView
            val itemCount = adapter?.itemCount ?: 0

            // Only continue if there are more than 1 item, otherwise, instantly return
            if (itemCount <= 1) return

            // Once the scroll state is idle, check what position we are in and scroll instantly without animation
            if (newState == SCROLL_STATE_IDLE) {
                // Get the current position
                val pos = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()

                // If our current position is 0,
                if (pos == 0) {
                    Log.d("AutoScrollingRV", "Current position is 0, moving to ${itemCount - 1} when item count is $itemCount")
                    scrollToPosition(itemCount - 2)
                } else if (pos == itemCount - 1) {
                    Log.d("AutoScrollingRV", "Current position is ${itemCount - 1}, moving to 1 when item count is $itemCount")
                    scrollToPosition(1)
                } else {
                    Log.d("AutoScrollingRV", "Curren position is $pos")
                }
            }
        }
    }

    init {
        addOnScrollListener(onScrollListener)
    }
}

For the Adapter, just make sure to update 2 methods, in my case, viewModels is just my data structure that contains the data that I send over to my ViewHolders

override fun getItemCount(): Int = if (viewModels.size > 1) viewModels.size + 2 else viewModels.size

and on ViewHolder, you just retrieve the adjusted index’s data

    override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
        val adjustedPos: Int =
            if (viewModels.size > 1) {
                when (position) {
                    0 -> viewModels.lastIndex
                    viewModels.size + 1 -> 0
                    else -> position - 1
                }
            } else {
                position
            }
        holder.bind(viewModels[adjustedPos])
    }

The previous implementation’s hurt me haha, seemed way to hacky to just add a crazy amount of items, big problem when you run into Multiple cards with an Integer.MAX_VALUE nested RecyclerView. This approach fixed all the problems of OOM since it only necessarily creates 2 and ViewHolders.

###

I have created a LoopingLayoutManager that fixes this issue.

It works without having to modify the adapter, which allows for greater flexibility and reusability.

It comes fully featured with support for:

  • Vertical and Horizontal Orientations
  • LTR and RTL
  • ReverseLayout for both orientations, as well as LTR, and RTL
  • Public functions for finding items and positions
  • Public functions for scrolling programmatically
  • Snap Helper support
  • Accessibility (TalkBack and Voice Access) support

And it is hosted on maven central, which means you just need to add it as a dependency in your build.gradle file:

dependencies {
    implementation 'com.github.beksomega:loopinglayout:0.3.1'
}

and change your LinearLayoutManager to a LoopingLayoutManager.

It has a suite of 132 unit tests that make me confident it’s stable, but if you find any bugs please put up an issue on the github!

I hope this helps!

###

Endless recyclerView in both sides

Add onScrollListener at your recyclerview

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() 
{
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            int firstItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
            if (firstItemVisible != 1 && firstItemVisible % itemList.size() == 1) {
                ((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPosition(1);
            }
            int firstCompletelyItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (firstCompletelyItemVisible == 0)
            {}

            if (firstItemVisible != RecyclerView.NO_POSITION
                    && firstItemVisible== recyclerView.getAdapter().getItemCount()%itemList.size() - 1)
            {
                ((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(itemList.size() + 1, 0);
            }
        }
    });

In your adapter override the getItemCount method

@Override
public int getItemCount()
{
    return itemList == null ? 0 : itemList.size() * 2 + 1;
}

Leave a Reply

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