I'm creating a chat app and I am trying to implement endless scrolling for RecyclerView in a reversed manner, like start from the bottom and scroll up, when top is reached, load more.
When the user opens the chat screen, the app gets the last 20 messages and scrolls to the bottom by default. I want to get more messages from the server as the user scrolls up.
The following is just a generic endless scrolling code i am testing with:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
layoutManager = new LinearLayoutManager(this);
recyclerView = (RecyclerView)findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
adapter = new RecyclerViewAdapter<String, ItemViewHolder>(String.class,R.layout.item,ItemViewHolder.class,list) {
@Override
protected void populateViewHolder(ItemViewHolder viewHolder, String model, int position) {
if(model != null){
viewHolder.textView.setText(model);
}
}
};
recyclerView.setAdapter(adapter);
recyclerView.setOnScrollListener(new EndlessReverseRecyclerViewScrollListener(layoutManager) {
@Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
loadMore();
}
});
loadInit();
}
private void loadInit(){
for(int x = 0; x < 20;x++){
list.add(list.size()+": "+getRandomString(8));
adapter.notifyDataSetChanged();
}
layoutManager.scrollToPosition(list.size() -1);
}
private void loadMore(){
for(int x = 0; x < 20;x++){
list.add(list.size()+": "+getRandomString(8));
adapter.notifyDataSetChanged();
}
}
endless scrolling class
public EndlessReverseRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
this.layoutManager = layoutManager;
}
// 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 = layoutManager.getItemCount();
if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null);
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
} else if (layoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
}
Log.d(TAG," *** last visible: "+lastVisibleItemPosition);
Log.d(TAG," *** total: "+totalItemCount);
// 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);
This works perfectly for scrolling from top to bottom, Can someone please assist on how I can make it work from bottom to top. Thanks
P.S. ReverseLayout is not the solution I'm looking for because it just reverses the order of my items and that's not what I am trying to achieve.
For new beginner who is facing problem, my solution is same as accepted answer but with my code snippet,
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setReverseLayout(true);
chatRecyclerView.setLayoutManager(linearLayoutManager);
for endless and smooth scrolling i used.. pagination library provided by android
The reverseLayout
property on a RecyclerView's LayoutManager will do what you want.
Kotlin:
myRecyclerView.layoutManager = LinearLayoutManager(
context = this,
orientation = LinearLayoutManager.VERTICAL,
reverseLayout = true
).apply { stackFromEnd = true }
XML:
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:reverseLayout="true"
app:stackFromEnd="true"
android:orientation="vertical />
You can set stackFromEnd
to false
if you don't want your items to stack from bottom.
I was facing the same concern since last few days and this what I come up with. Endless scroll listener for both directions (top/bottom).
Note: I haven't tested this for "GridLayoutManager".
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
// Sets the starting page index
private static final int startingPageIndex = 0;
// 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;
private RecyclerView.LayoutManager mLayoutManager;
private LoadOnScrollDirection mDirection;
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager, LoadOnScrollDirection direction) {
this.mLayoutManager = layoutManager;
mDirection = direction;
}
public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager, LoadOnScrollDirection direction) {
this.mLayoutManager = layoutManager;
visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
mDirection = direction;
}
public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager, LoadOnScrollDirection direction) {
this.mLayoutManager = layoutManager;
visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
mDirection = direction;
}
// 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(@NonNull RecyclerView view, int dx, int dy) {
int lastVisibleItemPosition = 0;
int firstVisibleItemPosition = 0;
int totalItemCount = mLayoutManager.getItemCount();
if (mLayoutManager instanceof StaggeredGridLayoutManager) {
int[] lastVisibleItemPositions =
((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
int[] firstVisibleItemPositions =
((StaggeredGridLayoutManager) mLayoutManager).findFirstVisibleItemPositions(null);
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
firstVisibleItemPosition = getFirstVisibleItem(firstVisibleItemPositions);
} else if (mLayoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition =
((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
firstVisibleItemPosition =
((LinearLayoutManager) mLayoutManager).findFirstVisibleItemPosition();
} else if (mLayoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
firstVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findFirstVisibleItemPosition();
}
switch (mDirection) {
case BOTTOM:
// 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 = 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);
loading = true;
}
break;
case TOP:
// 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 = 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 && firstVisibleItemPosition < visibleThreshold) {
currentPage++;
onLoadMore(currentPage, totalItemCount);
loading = true;
}
break;
}
}
private 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;
}
private int getFirstVisibleItem(int[] firstVisibleItemPositions) {
int maxSize = 0;
for (int i = 0; i < firstVisibleItemPositions.length; i++) {
if (i == 0) {
maxSize = firstVisibleItemPositions[i];
} else if (firstVisibleItemPositions[i] > maxSize) {
maxSize = firstVisibleItemPositions[i];
}
}
return maxSize;
}
// Defines the process for actually loading more data based on page
public abstract void onLoadMore(int page, int totalItemsCount);
public enum LoadOnScrollDirection {
TOP, BOTTOM
}
}
Implementation
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
layoutManager.setStackFromEnd(true);
mBinding.rvChats.setLayoutManager(layoutManager);
mBinding.rvChats.addOnScrollListener(new EndlessRecyclerViewScrollListener(layoutManager, EndlessRecyclerViewScrollListener.LoadOnScrollDirection.TOP) {
@Override
public void onLoadMore(int page, int totalItemsCount) {
mViewModel.fetchMessages(page);
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With