56

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

Community
  • 1
  • 1
ltsai
  • 757
  • 3
  • 8
  • 16
  • @DamianKozlak RecyclerView is not same as ListView – Chad Bingham Dec 30 '15 at 01:19
  • 1
    @ChadBingham Of course not, but this problem could be resolved using the same solution like in `ListView`. Both `ListView` and `RecyclerView` adapters got `getCount()` and `getItem()` methods. – Damian Kozlak Dec 30 '15 at 09:17
  • Did you find a solution? I am working on the same problem, currently I am thinking of writing a custom LayoutManager extending from LinearLayoutManager. Did you ever achieve your goal? – A. Steenbergen Jul 07 '16 at 16:59
  • yeah Sebastian's answer below worked for me. It was the simplest solution I found. – ltsai Jul 07 '16 at 21:20

7 Answers7

107

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);
    
Sebastian Pakieła
  • 2,970
  • 1
  • 16
  • 24
  • Hey, this is exactly what I do, but I cannot call smoothScrollToPosition() with negative value. I want my list can be dpad controlled and can scroll left direction as well as right direction. – anhtuannd Apr 04 '16 at 01:31
  • 1
    @anhtuannd Use MAX_INT / 2 or whatever as the starting position. – Tunga Aug 19 '16 at 13:00
  • 6
    Great solution! I added code for better understanding. :) – Sufian Sep 29 '16 at 12:50
  • 1
    Great idea! One improvement is to show the same first item after scrollTo() call. Could be: int number = Integer.MAX_VALUE / items.size() / 2; scrollToPosition(number * items.size()); – Artem Jul 19 '17 at 14:54
  • 3
    I don't understand how this work's, in first place we are talking about Recyclerview adapter ,from where does getItem / getView comes here. – geniushkg Sep 06 '17 at 12:00
  • 4
    @geniushkg That is actually simpler. You don't need to override any method, but just use another position (e.g. realPosition) where it = `position % itemList.size()` in your `onBindViewHolder()`. – Sira Lam Oct 25 '17 at 02:42
  • Hi, i'm still confused about the question asked by @geniushkg, lets say I declared (int myPosition = position % itemList.Size()) in my OnBindViewholder, where should i use myPosition ? – Arduino Nov 15 '19 at 02:34
  • Since when did a big number (2 billion) become infinite? First this is bounded meaning it will reach the end at some point, second it doesn't seem very memory efficient creating 2 billion cells.. – 6rchid May 13 '20 at 16:43
  • 1
    It does not have to be infinite. It just have to look like it. The thing it is does not create 2 billion cells. It is using same cells in a kind of loop using modulo operation. In terms of reaching the end you are right. Of course it is going to reach it, but for 2^31 and assuming scrolling one item is 0,5second, the end is reachable in 17years of scrolling. Good luck ;) – Sebastian Pakieła May 14 '20 at 20:32
  • @6rchid it's a recyclerview, it doesn't create all the memory. It reuses it when it needs to. – Mikkel Larsen Jul 16 '20 at 11:39
  • Great solution! Thanks Sebastian. – ucMedia Aug 06 '20 at 15:34
  • Some how `Integer.MAX_VALUE` does not work properly with reverseLayout and StackfromEnd True. – ADM Jan 19 '21 at 12:48
  • This solution handles it easily, but can the device crash due to ram usage? – Ümit Bülbül Feb 22 '21 at 08:07
  • use recyclerView.getLayoutManager().scrollToPosition((Integer.MAX_VALUE / 2)-(size-1)) for item starting from first position. – Muhammed Haris May 18 '21 at 05:40
  • For Accessibility is there a way to announce the items as "1 of 5, in list of 5 items" instead of "1072741820 of 2147483640, in list of 2147483640 items" ? – Danielson Jul 08 '21 at 22:13
  • Unfortunately this solution is not useful when your list could possibly have 0 items and then items are added. (After the items are added you will now find yourself at the very top of the list unable to scroll up.) – Ben Mora Aug 24 '21 at 01:35
  • 1
    onBindViewHolder running infinintely, and getting memory issue, how can I stop looping onBindViewHolder() – Muhammed Haris Sep 15 '21 at 07:12
28

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);
}
tochkov
  • 2,917
  • 2
  • 15
  • 21
Nikhil Gupta
  • 881
  • 6
  • 14
  • mine only repeated one time with this method, e.g. i entered dummy data 1,2,3,4, it only show 1,2,3,4,1,2,3,4, rather than keep looping infinitely, what did i do wrong ? – Beeing Jk Apr 02 '18 at 09:00
  • @BeeingJk in the getItemCount function, we are returning twice the elements in our list, that is why for you it is repeating only twice. Try returning 4-5 times the number of elements in your list then it should work properly. – Nikhil Gupta Apr 03 '18 at 03:27
  • thanks @NikhilGupta, i have figured it out, because of my data length is too short that `firstItemVisible % itemList.size() == 0` never happen, and yea returning 3 times make it work, but I can't guarantee it will work all the time so I do compare the total data length with RecyclerView width before enabling auto scroll, can't find an easier way so far – Beeing Jk Apr 03 '18 at 06:22
  • 3
    I am not able to loop back to the end from the start, but its is looping to the start from the end. Is there a way I can go either direction. – Prens May 29 '18 at 20:13
  • 2
    from where i need to call autoScroll() method please help. – umesh shakya Sep 06 '18 at 06:10
  • @umeshshakya You can call autoscroll from anywhere. Just make sure you have initialised your recyclerView and set adapter and other things you need to set. – Nikhil Gupta Sep 06 '18 at 09:43
  • Thanks @NikhilGupta ! Do you know a way to loop back to end from the start please? – NayMak Jan 08 '19 at 15:04
  • @NayMak for looping also backward use whats mentioned in the selected answer - recyclerView.getLayoutManager().scrollToPosition(Integer.MAX_VALUE / 2); after the recycler view is initiated and make sure your getCount method returns Integer.MAX_VALUE. – Itay Feldman Nov 23 '19 at 09:59
  • Amazing. Found exactly what I was looking for. For smooth scrolling without issues. – Muhammad Rafique May 05 '20 at 01:50
12

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!

Beks_Omega
  • 404
  • 1
  • 5
  • 13
  • This great but I am noticing an issue where items jump positions while scrolling. – Dave Thomas Mar 10 '20 at 21:22
  • 1
    Thank you for trying out my project @DaveThomas! I also really appreciate you reporting a bug about it. I've never seen that issue, so I can't give you advice immediately. But if you fill out an [issue](https://github.com/BeksOmega/looping-layout/issues/new?assignees=&labels=bug&template=bug_report.md&title=) I may be able to track it down. – Beks_Omega Mar 11 '20 at 21:47
  • 2
    For me the suggested library works like a charm. I appreciate that it also supports `LinearSnapHelper`! – Michal Vician Mar 28 '20 at 20:42
  • 1
    Now I see that the problem with `LoopingLayoutManager` is that it lays out/renders the very same `View` instance multiple times. At the first glance it looks great, but this approach doesn't count with the scenario if one needs to update the item e.g. `onClick`. In such situation the behavior is unspecified (a bit of a mess) because the items which were not clicked also updates their UI. This happens because the items share the same `View` instance. The aforementioned `Integer.MAX_VALUE % 2` approach might not be the cleanest one from the coding point of view, but works for this scenario. – Michal Vician Mar 29 '20 at 15:38
  • seems it doesn't support add or remove item, the LinearLayoutManager would auto scroll to next item after current item was deleted – VinceStyling Oct 19 '22 at 13:13
  • Works great! Thanks for sharing – Jonathan F. Jul 21 '23 at 07:56
2

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!

aphanite
  • 59
  • 1
  • 4
2

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.

mateog
  • 21
  • 2
0

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.

Daniel Kim
  • 899
  • 7
  • 11
-2

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;
}
Roman
  • 318
  • 2
  • 8