spongyboss
spongyboss

Reputation: 8666

RecyclerView reverse endless scrolling

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.

Upvotes: 11

Views: 8863

Answers (3)

Rainmaker
Rainmaker

Reputation: 11090

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.

Upvotes: 15

Abhishek Garg
Abhishek Garg

Reputation: 3242

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

Upvotes: 1

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);
            }
        });

Upvotes: 7

Related Questions