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.

Patrick Elmquist
Published in
5 min readNov 3, 2022

--

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:

Screen recording of scrolling 125 items using regular smooth scroll.
Default smooth scroll of 125 items. Duration 5.08s.

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:

Screen recording of scrolling 125 items using quick smooth scroll.
Quick smooth scroll of 125 items. Duration 1.1s.

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?

  1. First, this is based on using a LinearLayoutManager or one of its subclasses likeGridLayoutManager, so we check for the proper type of layout manager.
  2. We create the SmoothScroller to use. It’s hardcoded to the index 0 for the top of the list and applies a speedFactor 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.
  3. 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.
  4. 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 👋

--

--

Patrick Elmquist
Flat Pack Tech

Software Engineer@IKEA. Caffeinated unicorn of excellence, with an interest in UX and design