Quickly scroll to the top of a list
Learn how to smooth scroll to the top of a RecyclerView
in constant time no matter the number of items.
What’s the issue?
In the IKEA app we have product lists that tend to be rather long, some categories contain 1000+ items. When a filter is applied to a category we want to first scroll to the top of the list before applying the filter as the content will change. If the user has only scrolled down a couple of products this is not a problem, we can just use the built-in smooth scroll of the RecyclerView. However if the user has scrolled down say 100+ items, the scroll duration will quickly start to turn into a bad user experience. Just have a look at the example below where the user is scrolling past 125 items:
Using the default smooth scroll it take a little over 5 seconds to scroll past 125 items. As it’s using a LinearSmoothScroller
under the hood the time it takes will grow linearly like:
// list.smoothScrollToPosition(0)#Items Smooth scroll
125 5s
250 10s
500 20s
As the distance becomes longer the user experience quickly degrades as it takes longer and longer to reach the top. Already 5 seconds is stretching what the user should have to wait for such a trivial action, 10 and 20 seconds is just horrible. From here there are two obvious paths for solving the time issue:
- Use
list.scrollToPosition(0)
which results in an instant scroll to the top without any animation. This solves the time issue but is not a smooth experience for the user. - Create a subclass of
LinearSmoothScroller
that modify the scroll speed to meet a certain max time. For a medium size list of items this works pretty well, but for a long list the scroll speed will be so fast that it will start looking like the scroll is lagging and make the UI look janky. This is a bit hard to capture in a low quality gif but try cranking up the speed for a long list and it will be easy to spot.
So what to do?
Instead of adjusting the speed or smoothness to scroll past all items, let’s just scroll past a number of items that we have time for. Let’s define a threshold N that’s the maximum number of items we are willing to scroll past, then instantly scroll the list to N and smooth scroll from there. Something like this:
N = max items to scroll pastif number of items to scroll is < N
use regular smooth scroll
else
make a jump to item N from the top
use regular smooth scroll from N
So for a long list, this is what it would look like in practice:
The jump to N is done in a single frame and then the regular smooth scroll is started from N to the top. Basically the solution is to cheat, if the desired number of items can’t be scrolled within the desired time window, just ignore a part of the list and scroll whatever amount there’s time for. This is what it looks like in practice:
The time it takes to scroll the list of 125 items went down from 5 to 1.1 seconds. What is important to note is that it will remain at 1.1s even if the number of items is doubled or quadrupled as it will always just scroll the top N items of the list.
// list.smoothScrollToPosition(0)
// vs
// list.quickScrollToTop()#Items Smooth scroll Quick scroll
125 5s 1.1s
250 10s 1.1s
500 20s 1.1s
But hey now, hold on! That jump, won’t it be visible to the user? When looking real close at the screen recording it’s possible to spot the content change right as the scroll start, however it’s only noticeable when actively looking for it. The key for this to work well is that the colours of the items and the background look similar throughout the list, so the jump will be passed off as the scroll just starting and the previously visible items have just been scrolled away.
Enough talk, where’s the code?
With the end result demoed, time for some code. To be re-usable in multiple places it’s added as an extension function to RecyclerView
and looks like this:
What’s happening here?
- First, this is based on using a
LinearLayoutManager
or one of its subclasses likeGridLayoutManager
, so we check for the proper type of layout manager. - We create the
SmoothScroller
to use. It’s hardcoded to the index 0 for the top of the list and applies aspeedFactor
that allows the caller to control the speed of the scroll. This is not strictly needed but is a nice way to be able to fine tune the visual experience for different scenarios. - We check if the list is scrolled down more than the provided threshold of items. If so we perform a jump to the threshold location and wait a frame for everything to be laid out and measured before continuing.
- Start the smooth scroll with the custom scroller.
That’s all that’s needed for it to work.
LinearLayoutManager vs GridLayoutManager
Worth noting is that jumpThreshold
will assume that the list is a single column as no special handling for grids have been added to keep it simple, so for a grid just pass in:
jumpThreshold = rowsToScroll x columnsPerRow
Why suspend function?
Now what’s the reason for using a suspend
function? It’s purely based on preference. For the implementation there’s nothing stopping replacing awaitFrame()
with doOnPreDraw/doOnNextLayout
callbacks and have a regular function. However using coroutines
and suspend
functions allows for simple and declarative usage like:
viewLifecycleOwner.lifecycleScope.launch {
// scroll to top
list.quickScrollToTop() // wait for scroll to end
awaitScrollEnd() // make changes to the top of the screen
animateHeaderChange()
applyNewFilters()
}
Wrapping up
That’s all there is to it. Of course this can be improved further, allowing to scroll to the bottom or middle of the list or add better built-in support for grids, but that’s left as an exercise to the reader 😉 Have a good one 👋