Three image placeholders being scrolled through with a custom page indicator
Apps

How To Build a Custom Pager Indicator In Jetpack Compose

Lesezeit
9 ​​min

In this article, I describe what challenges you may face when you use the Accompanist Pager library when creating a custom pager indicator for a pager built in Jetpack Compose.

Recently, I had the chance to create a custom pager indicator for a pager built in Jetpack Compose using the Accompanist Pager library. Since the library is relatively new and there is not a lot to be found online apart from the official documentation, I would like to share the challenges I faced along the way.

You can find the entire source code for this article on GitHub.

I used the latest stable version of the Accompanist Pager library which is 0.23.1. Please note that the APIs still change quite a bit (as expected with 0.x versions). At the time of writing, there is a release candidate for version 0.24.13. At the end of this article, there is a quick outlook on the coming changes.

So, let’s get into it.

Pager in Compose

Building pagers in Compose is very straightforward and the official documentation gives you everything you need to know. So let’s just start with a basic pager showing one image per page:

(The pictures are taken from pixabay.com.)

I suppose this does not need much explanation – feel free to play around with it in the sample project.

PagerIndicator

Now, in order to see which item of the pager is currently selected, we would like to have a pager indicator. Adding one is fairly simple using the documentation. However, the indicators that come with the library are very limited. You can customize the colors, spacing and indicator shape, but that is about it.

A few examples (which can also be found in the sample project, of course):

picture of pyramids

What I wanted to build was something like this:

Gif with pictures swiping

Seems simple enough, doesn’t it? Well, unfortunately, it is not …

While the default indicator just moves an item around on a static background, here we need to actually change two items simultaneously. When changing pages, the previous item gets smaller and the next one gets larger.

To achieve this, we have to – you guessed it – build a custom pager indicator. And we are doing it with Jetpack Compose.

Build your own pager indicator with Jetpack Compose

First try: Just calculate the current and target width

The PagerState gives us the current page number, the target page number and an offset. Using that, it should be fairly easy to calculate the width of the current and target page (all other widths are constant anyway).

So, we are calculating the width of the current and target pages based on the currentPageOffset. The calculation is quite straight-forward:

  • If there is no offset at all, the current page is 5 times as wide as all the other pages.
  • During scrolling, as the offset gets bigger, the width of the current page gets smaller while the width of the target page gets bigger. In the end, the current page has the same width as all inactive pages while the target page has 5 times the width of the other pages.

In the rest of the code, we just draw boxes with background and spacing for all the pages we need.

Gif showing a half-swipe of several pictures nect to each other

Problem: Glitch when aborting a scroll

Now, this already works quite well – the sizes are (almost) adjusted correctly. However, there is a glitch in the rendering as can be seen above: When aborting a scroll, i.e., starting to change the page but stopping the drag before the page actually changes, then the width of the current and target pages (and, by extension, the entire indicator) is too small for a moment.

It took me a while to figure this out: The issue is that when scrolling through the pages, the target and current page numbers change. Let’s say we start scrolling from page 2 to 3: In the beginning, currentPage is 2 while targetPage is 3, just as expected. But, if we lift our finger while being closer to page 2 than to page 3 (meaning that the pager is supposed to animate back to page 2 again), currentPage and targetPage both point to 2. Of course, this makes sense because, at that moment, the target of the scroll is actually 2, but it complicates adjusting the width of page 3.

Second try: Calculate targetPage based on offset

So it seems that relying on targetPage from PagerState is not possible for this. So let’s calculate it on our own:

Now, targetPage always points to the “original“ target of the scroll, even if the user has stopped scrolling at a point which means that the pager will move back to the current page.

This helps and in most cases, it all works fine now (feel free to try it using the sample project).

Problem: Huge indicators when scrolling quickly

However, when scrolling quite fast and with the correct timing between the drag motions, an odd effect can be seen:

Gif showing fast scrolling pictures

Actually, there was another assumption that I made above (without actually mentioning it): The offset is at all times supposed to be somewhere between -1 and 1. Bad news: it is not. When scrolling quickly, the absolute value of the offset can actually be greater than 1. In that case, currentPage somehow does not keep up with the scrolling. The logs can look something like this:

We see that currentPage is stuck at 2 but the offset continues to grow until at some point, the scroll settles and currentPage is updated to 0.

Solution: Calculate currentPage based on offset

Apparently, we need to calculate currentPage ourselves using the integer part of the offset:

Using our calculated value for currentPage, the indicator finally behaves as expected:

Gif showing scrolling pictures

As a small optimization, we can use animateDpAsState for the width of the items. This makes the page changes a bit smoother.

Conclusion

I am quite happy with the solution for this custom PagerIndicator. Also, the journey to get there was fun – it felt a bit like getting your hands dirty which does not happen very often with all the great libraries that offer everything you need.

That being said, the solution feels rather complicated for such a seemingly simple problem. I suppose that most users are happy with the customization that the default indicator offers. But for those who need more, there is certainly room for improvement in the Accompanist Pager library.

As mentioned in the beginning, the code for this blog post uses version 0.23.1 of the Pager library. To get a complete picture, we should also take a look at the upcoming changes.

Outlook

Currently, there is a release candidate of version 0.24.13 for the Pager library. So let’s find out how the library’s behavior changed in the meantime.

First of all, the new version does not have any breaking changes – I created a branch in GitHub which runs just fine.

There are, however, a few notable differences:

  • The glitch in the first version still persists. Actually, that is what I expected since the change of currentPage during the scroll makes sense.
  • The second glitch is not reproducible. The reason for this is that currentPage is updated immediately when scrolling past half of the page (for details, check out the pull request). Apparently, this strange behavior was indeed a bug that is already fixed in the upcoming version.
  • Also, targetPage is deprecated (pull request). So, in the future, we would need to calculate the target page ourselves anyway.

In conclusion, the update fixes the issue with absolute offset values >= 1 which makes implementation of a custom pager indicator as described in this article easier. But still, there is some manual effort to be done for calculating the targetPage.

Hopefully, this article helps some people out there. I am looking forward to any comments or questions you might have!

Hat dir der Beitrag gefallen?

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert