The Guide to Infinite Loop Carousel with RecyclerView
So recently I was tasked to make a carousel list that loops infinitely when scrolled. It means that when we scroll to the last item on the list, the next item should loop back to the first item again.
I think the “infinite” concept itself is not hard to understand. What is hard to understand is how we can represent that “infinite” in our code and show it to user. Do we need to create infinite list of items? How can we store infinite list of items into finite memory?
The answer is no. We don’t need to create infinite list of items. I don’t even think we can create infinite list of items in our code without encountering something like StackOverflowError
, or OutOfMemoryError
. Memory is finite, so at least for now it is impossible.
What we actually need is a trick to make it seems like the list is looping infinitely to the user when they are scrolling. And how do we achieve that?
Based on my knowledge, there are 3 ways to achieve this:
1. Manipulating Adapter’s getItemCount()
I won’t dive deep into this one as this is one of the most recommended solution you will find if you google, so you can find it easily by yourself or you can just read how to implement it here.
The goal of this method is to trick RecyclerView.Adapter
into believing that we have a big amount of items even though we only have a limited of items. This means that it’s not even infinite and if the users keep persistently scroll the RecyclerView, they will eventually reach the end of the list.
Besides that, there’s another problem with this approach. The fact that it manipulates only the Adapter’s getItemCount()
method while still using the original number of item in the list, makes it inconsistent and unusable with DiffUtil.
You can still call notifyItemXXX(position)
method by yourself but you need to really know the current adapter position of the items that appear on the screen so that they will also change to the new data, or maybe just use notifyDataSetChanged
if it’s possible.
Pros:
- Very easy to understand and implement
Cons:
- Only good for static items (items’ states that do not change, because you don’t need to notify anything if they never change) as it becomes tricky when the state of the items on the list change
- There’s a possibility for user to scroll to the edge of the list
2. Manipulating the list of items and RecyclerView’s scroll
The goal of this method is to secretly move the current item position when it is getting near/past the edge of the list. We also need to add some “fakes” or “clones” of our original/real data to the start and end of them.
So let’s say we have a list containing 5 items.
As we need to add some clones to the start and end, it will be come something like this:
[1' 2' 3' 4' 0 1 2 3 4 0' 1' 2' 3']
(the '
indicates the cloned int)
Because we added some clones to the start of the list, we need to scroll to the first index of the real data after we set the items to the adapter for the first time.
The idea is that when the user scrolls to the left of the leftmost real item (from 0
to 4'
) or to the right of the rightmost real item (from 4
to 0’
), we will secretly move the scroll position so that it stays in the bounds of the real item.
So let’s say when we scroll to the left of the 0
(which is 4’
), when the 4’
is visible on the screen, we can force scroll the RecyclerView by some X value so that it will move to the 4
. And it will keep on going like that if the we keep scrolling to the left. Same goes for when we scroll to the right of 4
(which is 0'
), but instead this time we force scroll the RecyclerView so that it will move to the 0
.
The force scrolling that we need to do have to be precise so that it looks seamless to the eye of the users.
The code to handle that would roughly be something like this:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
val lastVisiblePos = layoutManager.findLastVisibleItemPosition()
if (firstVisiblePos < startOfRealItemIndex) {
recyclerView.scrollBy(somePositiveXValue, 0)
} else if (lastVisiblePos > endOfRealItemIndex) {
recyclerView.scrollBy(someNegativeXValue, 0)
}
}
}
This code will give the illusion of non-ending scroll either to the right or to the left.
The problem is that if we fling too fast, we might be able to reach the edge of the RecyclerView. Several solutions to that will be:
- Adding more number of fakes/clones to each side
- Limiting the fling speed of the RecyclerView
Here is a sample project that I created to showcase this method.
Pros:
- Gives the illusion of real infinite list
- Can be used with DiffUtil or any other
notifyItemXXX()
easily
Cons:
- A little bit hard to understand
- Might need to limit fling speed of RecyclerView (which might impact users’ interaction with the list)
- A quite heavy logic on
RecyclerView.OnScrollListener#onScrollStateChanged
3. Create a custom RecyclerView LayoutManager
I personally haven’t tried this one but it is definitely possible to create a custom LayoutManager that can give the illusion of infinite looping. In fact, I think this method is the most proper (and probably the coolest) method out of all three.
Creating a custom RecyclerView LayoutManager definitely needs extra efforts, but the possibility is wide. The problem is, creating a custom LayoutManager is pretty hard as we need to control many things, from how to layout the items to when to recycle and use the recycled ViewHolder(s) by ourselves.
Here is a great article that may guide you on how to make a custom layout manager. Or maybe you can just find a working library in Github.
Pros:
- More flexibility and possibility
- Leave your data untouched
Cons:
- Harder to implement
Those 3 methods can help you achieve infinite loop carousel list, which one you need to use will all depends on the requirement and the time you got. I personally used the second method as the items that I worked on have states that can change, and I didn’t have time to create a custom LayoutManager.
That’s it for this time and feel free to share your experience on dealing with this particular problem.