63

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 ?

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • shouldn't you use RecyclerView.AdapterDataObserver https://developer.android.com/reference/android/support/v7/widget/RecyclerView.AdapterDataObserver.html – Krupal Shah Sep 20 '15 at 11:12
  • @KrupalShah I don't think it tells me about it after the views are shown. I think it's called only when the data has changed. Sadly I don't see documentation for it, so I've now tested it, and it doesn't help (shows both first item and last item is "-1") , maybe because the RecyclerView is a part of a fragment within a viewPager. However, when the current page use this, I first get a bad result and then a good result (because I do need to change the data soon after the page is shown). Maybe I can check the items for when changing a page AND using what you've written. – android developer Sep 20 '15 at 11:35
  • 3
    `RecyclerView.LayoutManager#onLayoutChildren` – pskink Sep 20 '15 at 11:46
  • @pskink This is almost perfect. It does gets called on each time I need, but it also gets called when scrolling (but for some reason stops getting called while scrolling). Maybe it's good enough. Have a +1 for now. – android developer Sep 20 '15 at 11:54
  • @pskink ok, since I can't find a better solution, I will show how to use yours. It does gets called more times than I need, but it's not as often as scrolling, and it occurs even when the keyboard is shown (which is great). – android developer Sep 20 '15 at 12:18
  • 1
    RecyclerView.LayoutManager#onLayoutCompleted(RecyclerView.State state) – ANemati Jul 12 '16 at 20:13
  • @ANemati Have you tried it with what I asked about? – android developer Jul 13 '16 at 05:20
  • @androiddeveloper this one also will get called multiple times (less frequent tho) but in my case I only needed it for the first time so with a flag in the activity (you can reset it in onCreate or onResume) I could achieve my goal. – ANemati Jul 26 '16 at 18:30
  • @ANemati Can you please show it in an answer, and also offer a push-request in the github? – android developer Jul 26 '16 at 19:39

4 Answers4

58

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);
    }
});
mortalis
  • 2,060
  • 24
  • 34
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • 1
    The basic assumption mAdapter.getItemCount() > itemsShown seems to be wrong for me. What if u have more items that screen can display? For example mAdapter.getItemCount() is 40 and max displayable items are 10. So mAdapter.getItemCount() > itemsShown is always true. Correct me if i am wrong – Phate P Apr 04 '17 at 15:14
  • @PhateeP That's the idea. If there are too many items than what it can show, I show the fastScroller, and if not, I hide it. – android developer Apr 05 '17 at 15:07
  • This is exactly what I needed to use when the user changes the sort order of the items, in which case it feels natural to be brought back to the top of the list. With this solution the scroll is seamless, much better than it was when using a short timer to delay the scroll after the data submission, as suggested in answers to similar questions. – Ronan Mar 14 '19 at 23:42
  • 1
    @androiddeveloper wouldn't it be a better idea to use [`onLayoutCompleted(State state)`](https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager.html#onLayoutCompleted(android.support.v7.widget.RecyclerView.State) instead as it is only called once, instead of many times like the on you used? – Sufian Sep 06 '19 at 10:16
  • @Sufian I think the question was asked when it was version 23.1.1. What you offer is on 24.1.0. It wasn't available back when I asked it. Anyway, thank you. I've updated the code of the repository, and it seems to work well. Also updated the answer – android developer Sep 07 '19 at 21:53
1

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.

Howard
  • 53
  • 2
  • 9
Peter
  • 10,910
  • 3
  • 35
  • 68
  • I don't think it will work. First of all, it sets null value after first run. Also, even if you fix this, I don't think it will get called on next times, when the RecyclerView changes its number of items, even down to zero, when the fast-scroller should disappear. – android developer Feb 11 '18 at 00:15
  • 1
    It doesn't work, the layout may be finished before the adapter is populated. Moreover, use `removeOnGlobalLayoutListener` instead of the hack `recyclerViewReadyCallback = null`. – RedGlyph Oct 20 '21 at 16:10
0

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
                    }
                }
            }
        };
Francois
  • 10,465
  • 4
  • 53
  • 64
  • So when can it be useful? It will get called multiple times, but the first time that you need to adjust things to it, it won't be called... – android developer Nov 06 '19 at 15:17
  • For instance, when you have a recycler view that shows only one item at a time and you want the position after the scroll has completed, using `yourRecyclerView.getLayoutManager().findFirstVisibleItemPosition();` – Francois Nov 07 '19 at 17:11
  • I guess you reached this need, then. :) – android developer Nov 07 '19 at 20:38
0

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