Is there a callback for when RecyclerView has finished showing its items after I've set it with an adapter?

AndroidScrollbarAndroid RecyclerviewFast Scroller

Android Problem Overview


Background

I've made a library that shows a fast-scroller for RecyclerView (here, in case anyone wants), and I want to decide when to show and when to hide the fast-scroller.

I think a nice decision would be that if there are items that aren't shown on the screen (or there are a lot of them that do not appear), after the RecyclerView finished its layout process, I would set the fast-scroller to be visible, and if all items are already shown, there is no need for it to be shown.

The problem

I can't find a listener/callback for the RecyclerView, to tell me when it has finished showing items, so that I could check how many items are shown compared to the items count.

The recyclerView might also change its size when the keyboard appears and hides itself.

What I've tried

The scrolling listener will probably not help, as it occurs "all the time", and I just need to check only when the RecyclerView has changed its size or when the items count (or data) has changed.

I could wrap the RecyclerView with a layout that notifies me of size changes, like this one that I've made, but I don't think it will work as the RecyclerView probably won't be ready yet to tell how many items are visible.

The way to check the number of items being shown might be used as such:

    final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
    mRecyclerView.setLayoutManager(layoutManager);
    ...
    Log.d("AppLog", "visible items count:" + (layoutManager.findLastVisibleItemPosition() -layoutManager.findFirstVisibleItemPosition()+1));

The question

How do I get notified when the recyclerView has finished showing its child views, so that I could decide based on what's currently shown, to show/hide the fast-scroller ?

Android Solutions


Solution 1 - Android

I've found a way to solve this (thanks to user pskink), by using the callback of LayoutManager :

final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false) {
            @Override
            public void onLayoutChildren(final Recycler recycler, final State state) {
                super.onLayoutChildren(recycler, state);
                //TODO if the items are filtered, considered hiding the fast scroller here
                final int firstVisibleItemPosition = findFirstVisibleItemPosition();
                if (firstVisibleItemPosition != 0) {
                    // this avoids trying to handle un-needed calls
                    if (firstVisibleItemPosition == -1)
                        //not initialized, or no items shown, so hide fast-scroller
                        mFastScroller.setVisibility(View.GONE);
                    return;
                }
                final int lastVisibleItemPosition = findLastVisibleItemPosition();
                int itemsShown = lastVisibleItemPosition - firstVisibleItemPosition + 1;
                //if all items are shown, hide the fast-scroller
                mFastScroller.setVisibility(mAdapter.getItemCount() > itemsShown ? View.VISIBLE : View.GONE);
            }
        };

The good thing here is that it works well and will handle even keyboard being shown/hidden.

The bad thing is that it gets called on cases that aren't interesting (meaning it has false positives), but it's not as often as scrolling events, so it's good enough for me.


EDIT: there is a better callback that was added later, which doesn't get called multiple times. Here's the new code instead of what I wrote above:

        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false) {
            @Override
            public void onLayoutCompleted(final State state) {
                super.onLayoutCompleted(state);
                final int firstVisibleItemPosition = findFirstVisibleItemPosition();
                final int lastVisibleItemPosition = findLastVisibleItemPosition();
                int itemsShown = lastVisibleItemPosition - firstVisibleItemPosition + 1;
                //if all items are shown, hide the fast-scroller
                fastScroller.setVisibility(adapter.getItemCount() > itemsShown ? View.VISIBLE : View.GONE);
            }
        });

Solution 2 - Android

I'm using the 'addOnGlobalLayoutListener' for this. Here is my example:

Definition of an interface to perform the action required after the load:

public interface RecyclerViewReadyCallback {
  void onLayoutReady();
}

on the RecyclerView, I trigger the onLayoutReady method when the load is ready:

mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
  if (recyclerViewReadyCallback != null) {
    recyclerViewReadyCallback.onLayoutReady();
  }
  recyclerViewReadyCallback = null;
});

Note: The set to null is necessary to prevent the method from being called multiple times.

Solution 3 - Android

Leaving this here as an alternate approach. Might be useful in some cases. You can also make use of the LinearLayoutManagers onScrollStateChanged() and check when the scroll is idle.

One thing to remember, when you load your view for the 1st time, this will not be called, only when the user starts scrolling and the scroll completes, will this be triggered.

LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(),
                RecyclerView.HORIZONTAL, false) {

            @Override
            public void onScrollStateChanged(int state) {
                super.onScrollStateChanged(state);

                if (state == RecyclerView.SCROLL_STATE_IDLE) {
                        // your logic goes here
                    }
                }
            }
        };

Solution 4 - Android

The solution that works for me. Needed to do some stuff after RecyclerView was inited with items.

adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
    override fun onChanged() {
        viewModel.onListReady()
        adapter.unregisterAdapterDataObserver(this)
    }
})

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionandroid developerView Question on Stackoverflow
Solution 1 - Androidandroid developerView Answer on Stackoverflow
Solution 2 - AndroidPeterView Answer on Stackoverflow
Solution 3 - AndroidFrancoisView Answer on Stackoverflow
Solution 4 - AndroidВладислав СтариковView Answer on Stackoverflow