Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 960
spongyboss Avatar asked Dec 08 '17 16:12

spongyboss


3 Answers

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

like image 159
Abhishek Garg Avatar answered Sep 18 '22 12:09

Abhishek Garg


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.

like image 35
Rainmaker Avatar answered Sep 20 '22 12:09

Rainmaker


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);
            }
        });
like image 41
Pasan Randula Eramusugoda Avatar answered Sep 19 '22 12:09

Pasan Randula Eramusugoda