Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nesquena/d09dc68ff07e845cc622 to your computer and use it in GitHub Desktop.
Save nesquena/d09dc68ff07e845cc622 to your computer and use it in GitHub Desktop.
Endless RecyclerView scrolling for different layout managers
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
// The minimum amount of items to have below your current scroll position
// before loading more.
private int visibleThreshold = 5;
// The current offset index of data you have loaded
private int currentPage = 0;
// The total number of items in the dataset after the last load
private int previousTotalItemCount = 0;
// True if we are still waiting for the last set of data to load.
private boolean loading = true;
// Sets the starting page index
private int startingPageIndex = 0;
RecyclerView.LayoutManager mLayoutManager;
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
}
public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
}
public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
}
public int getLastVisibleItem(int[] lastVisibleItemPositions) {
int maxSize = 0;
for (int i = 0; i < lastVisibleItemPositions.length; i++) {
if (i == 0) {
maxSize = lastVisibleItemPositions[i];
}
else if (lastVisibleItemPositions[i] > maxSize) {
maxSize = lastVisibleItemPositions[i];
}
}
return maxSize;
}
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
int lastVisibleItemPosition = 0;
int totalItemCount = mLayoutManager.getItemCount();
if (mLayoutManager instanceof StaggeredGridLayoutManager) {
int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
} else if (mLayoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
} else if (mLayoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) {
this.loading = true;
}
}
// If it’s still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && (totalItemCount > previousTotalItemCount)) {
loading = false;
previousTotalItemCount = totalItemCount;
}
// If it isn’t currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
currentPage++;
onLoadMore(currentPage, totalItemCount, view);
loading = true;
}
}
// Call this method whenever performing new searches
public void resetState() {
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = 0;
this.loading = true;
}
// Defines the process for actually loading more data based on page
public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
}
@alphater
Copy link

Hey, great code But I encountered a problem during use: when loading more, it will be repeated many times, I do not know if it is a case, so I modified the code, added an identifier to determine whether Loaded then reset this identifier in resetState.

@sharukhmohammed
Copy link

This doesn't work with NestedScrollView, all the pagination is happening at once, without user intervention.
Waiting for a solution for this great snippet.

@mapo-lp
Copy link

mapo-lp commented Oct 4, 2018

This doesn't work with NestedScrollView, all the pagination is happening at once ...

@wangzhiyuan
Copy link

This doesn't work with NestedScrollView, all the pagination is happening at once ...

I also met

@mobimation
Copy link

mobimation commented Oct 29, 2019

Replying to 2017 post :-) but useful to mention:
BenjaminRichardson asked on 22 Sep 2017:

Why do we need the concept of a page? It seems like you could pass along the current total item count or something similar and let the loadNextDataFromApi function determine what "page" needs to be retrieved.

Related to that, I'm curious why you chose to have the scroll listener dictate what is to be retrieved instead of loadNextDataFromApi tracking where it left off and returning the next set of data. I guess they could both work, I was just curious if I missed something."

This is a "convenience pattern" to play part of implementing apps with a seamless scrolling mechanism.
Usually a provider of JSON data have data organized as a huge array of entries.
A server side API pattern for retrieval is to specify a "size" parameter to specify how many entries you want.
The API interprets the size as the desired size of a "page".
Typical example request URL could be http://nicecompany.com/v6/entities?format=json&size=40
This means you are using a "version 6" of the API as given by the v6 path..
You are here requesting 40 entries of data for some list.
In the returned data you will typically find an array of the 40 entries you requested,
and a pagination record such as "pagination".
It will hold some values intended to help out with the view recycling:
page (current page is "1" since you did not specify one in the URL).
size (the amount if array entries returned. If the amount available is less than 40 then you see this here).
totalhits (the total amount of available entries)
totalpages (the total amount of pages available based on your given amount of 40)
nextpage: An URL that will return the next 40 entries. Notice how your specified page amount is being used
by the API to divide the total data set into pages. The last page may have a lower amount of entries than 40 but > 0.
in case you have reached the last page there will not be any "nextpage" element shown.
Example: nextpage : http://nicecompany.com/v6/entities?format=json&size=40&page=3
This simply returns the amount of entries you need, computed for you based on the page size you specified.
previouspage: In case you have specified a "&page=2" or higher page number part of the URL you will then find a
previouspage element that contains an URL that will give you the previous dataset. This arrangement supports
the view recycling scheme in modern user interfaces where as you scroll your list reach a position
where it is time to fetch data for the next or earlier set of list entries to show. When you give this URL to your
asynchronous background loading service it will begin fetching the array of entries so they are already
available ahead of the point in time when the list eventually reaches the position when the data is needed.
Your recycling handler code then replaces some older piece of cached data with the new data and your list can display the
list as if there is an endless list of entries loaded.

So this server side scheme is playing along with the list implementation pattern to accomplish a seamless user experience.

The basic reason for the paging solution is that the device displaying the list typically does not have enough RAM to hold lots
of data and only the currently visible portion and some amount around that is needed at every moment to satisfy the list component data needs. So they must be fetched in portions that represents a window into the total set of data.

@xxjonesriderxx
Copy link

xxjonesriderxx commented Dec 15, 2019

This does not work with .setStackFromEnd(true)
if you want to add new items to the top wenn swipping down (scrolling up) this do not work propperly

@CoffeeWarrior
Copy link

Thank you for this bit of code & especially the comments in it. I was trying to get a deeper understanding of how this works & the comments really help a great deal :) 👍

@ShareemGelitoTeofilo
Copy link

Awesome code! Thank you for this. Keep up the good work.

@TaslimOseni
Copy link

support add footerview(progressbar) https://gist.github.com/junfengren/f06c6f19e5832c62fe93b61ac8f72ff9

This URL is broken.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment