Building Google Contacts Screen and its Scrolling Bubble Feature - in Compose

Keith Abdulla
9 min readMay 2, 2022

This article assumes you know a bit of Compose and have a good idea what recomposition is. I am going to do a deep dive and take a complex feature & teach you how to build it in Compose. We won’t be building the exact Google Contacts UI and will mainly focus on the Scrolling bubble for simplicity. I’ll leave some features for you to try & build on your own!

Before starting, I’d recommend opening up the Google Contacts app and play around with the scrolling bubble feature. You’ll need to scroll from the edge of your device and have a minimum amount of contacts to see it.

the scrolling bubble feature in Google Contacts

Alright, let’s build this thing! To help better follow along I suggest to keep the contacts app open for constant reference

The Compose Approach

Let’s take an approach to this exercise by understanding what the different types of components there are. Here, there are 4 main ones.

  1. The main container that’s holding the contacts
  2. The contact items themselves
  3. The sidebar
  4. The floating bubble

For each of these components we can view them as their own Composable’s! For simplicity I’m going to replace the sidebar, with a list of verticals characters for easier visibility, the AlphabetScroller.

Example of the list of vertical characters on the right edge.

Believe it or not, we are just going to build off of this!

One thing to note in the code is the usage of Box — think of it like a FrameLayout, it’s generally used for a single child, but if you add more children they’ll start drawing over each other by default and thus ordering matters. Therefore, it’s important that the Bubble is declared after the Row. Technically, you could reverse the order and apply a z-index modifier to the bubble, but let’s leverage the fundamentals of Box.

Intro to the AlphabetScroller

The AlphabetScroller is a list of Text with each one representing a unique alphabet Char. The important thing to note here is the specific height for the Text composable, alphabetItemHeight — we’ll need to reference this later on when we dealing with user input.

Building the ContactItem in ContactList

In the Contacts UI we see that if there are multiple names with the same starting character, the first person in that list has a character left of their name. Then for those that weren’t first in their group — they still maintain the spacing on their left to ensure the text of names are aligned.

Identifying this we can add as a parameter to the ContactItem composable something like isAlphabeticallyFirstInCharGroup: Boolean .

Some things to note here are:

  1. verticalAlignment in the Row is used here to ensure each composable in the row is vertically centered since they vary in height — so if they didn’t vary in height we wouldn’t have to use it.
  2. isAlphabeticallyFirstInCharGroup is checked within the box since when it is false, we’d still have this spacing as we brought up before that we needed to keep alignment.
  3. We are using Surface to:
    - apply the circle shape like we see in the Contacts app around the first character of the name
    - be able to have the right content color on the background that was specified for accessibility, ie) it calculates the correct content color when in dark/light mode.
  4. We introduced Box inside the surface to center the text in the middle of the circle, because Shape does not provide any alignment mechanism, it is used purely for styling.

Building the ContactList

Now that we created our ContactItem composable we need to figure out a way to provide the isAlphabeticallyFirstInCharGroup and a way to do this is by having a map of each first seen character in its group, to the index of where it was seen. We can create a function like getMapOfFirstUniqueCharIndex which takes the list of contacts, lowercases the text, and produces a Map<Char, Int>.

With this new function returning Map<Char,Int> we can pass this data to our ContactList and as we iterate over the list of Contacts we will check the names first letter. If the index of the name is equal to the Int value when accessing theMap , we know isAlphabeticallyFirstInCharGroup is true.

For our Contact List, I intentionally used LazyColumn to hold the list of ContactItem . LazyColumn comes with all sorts of benefits such as dealing with a variable amount of items, item diffing (leveraged only when passing a key for each item into itemsIndexed) which is used when items are added/remove/moved in the list, and we can use the LazyListState passed to LazyColumn to scroll to a specific position in the column.

Contact List + Alphabet Scroller

In the designs, we saw that the Contact List is left of the Alphabet scroller and thus indicates that we need to package these two Composables into a row.

Things to note here are

  1. We don’t want to keep generating the map on each recomposition, but we do want to regenerate the map when the contacts list has changed. And to do this, we used the remember function with the key being the contacts list.
  2. AlphabetScroller doesn’t usually take the entire height of the screen and we want to vertically center it on the screen. Therefore, on the Row we’ve added a verticalAlignment to do this.
  3. The ContactList would take the entire height if we had enough contacts, but let’s say we have a couple Contacts then without passing the Modifier.fillMaxHeight() to the ContactList these contacts would be in the center of the screen.
  4. Lastly, the Modifier.weight(1F) passed to the ContactList enables it to take the remaining width of the screen that the AlphabetScroller didn’t take.

The Scrolling Floating Bubble

Now that we’ve built the main content, let’s take a look again at the high level outline of the code. Remember, we declared the Bubble composable after the ContactListWithScroller within a Box so that the bubble we be drawn over the list.

So we know how to create the effect of drawing the bubble over the main content — but what we need now is to know when and where to draw it.

And we draw it when a users finger is on the alphabet scroller and we draw it where the user finger is. So immediately we should think about how to get user input from the Alphabet scroller we created.

In order to get user input, specifically vertical dragging on the alphabet scroller we can use Modifier.pointerInput. It provides us a PointerInputScope and in this scope there are various helpful methods we can use like detectDragGesturesAfterLongPress , detectHorizontalDragGestures and the one we use is detectVerticalDragGestures! With this method we can track just vertical drags & specifically when the drag started, ended, and when it’s actively being dragged.

However, note that the reported y values are relative to the top of the alphabet list container and not relative to the screen. Given the alphabet container is wrapped and doesn’t fully extend to the top & bottom of the screen — if I tapped on the top of the char A cell, the y value reported would be 0, even though it might look like it’s 50dp from the top of the screen. Therefore, we need to figure out the distance of the top of the screen to the top of the alphabet container for later measurements.

To get this distance we can use onGloballyPositioned which gives us LayoutCoordinates . There’s lots of interesting things to unpack about LayoutCoordinates so I’d urge you to read more about it, but to keep it short there are helpful functions & properties given by it about the measured bounds of the Composable. In this case, we call positionInRoot(), which gives us a global Offset. This offset’s y property can be interpreted as the distance from the top of the screen to the top of the alphabet container.

With this information, the AlphabetScroller must communicate these values to its parent and the way we are communicate these is by passing a function into AlphabetScroller that the parents provide.

When the parents get the values from onAlphabetListDrag, they need to use them to control when to show the ScrollingBubble and where to draw the bubble. But these values are coming back via callbacks… so we need to store these values somewhere and pass the reference of this stored value to other composables.

Therefore, we can create mutableState of the offsets, reference it as an input to ScrollingBubble, and use it to indicate if we should show the bubble. Then when this state changes because of user’s interaction, we are able to leverage Compose’s recomposition where the UI reflects the updated values.

So when users aren’t interacting with the scroller, the yOffset is null and there will be no ScrollingBubble drawn but when there is a value — we can draw the bubble and at/near the y location.

Drawing the ScrollingBubble at a particular location

Now we’ve only brought up about the y location of the bubble — what about the x location of the bubble? The Contacts UI has the bubble close to the users finger, which in this case is the far end of the phone. In other words, the x location seems it should be close to being the max width of the phone, if 0 is the start and maxWidth is the far end of the screen. So how do we determine maxWidth?

There are multiple ways to get this information. One way is by calling LocalConfiguration.current.screenWidthDp , but if we wanted more flexibility over what maxWidth means. We can change our top level Box to a BoxWithConstraints . In the body of BoxWithConstraints, we are returned a BoxWithConstraintsScope and can access its maxWidth property!

It is important to note the BoxWithConstraints must spread as wide as the phone to get the correct width of the screen. If the box was wrapped to the content and that content’s width was half of the screen, then the maxWidth would be half of the screen.

With the maxWidth of the screen and the bubbleOffsetY from the drag listener, we can now appropriately place the bubble where we want it.

The scrolling bubble is currently being drawn at the origin of the screen — x=0, y=0 — and therefore with the values we’ve gathered we can use Modifier.offset to place the bubble where we want.

Experience of the app with the snippet above.

For the X value, we want the Bubble to be on the left of the alphabet scroller… so what we can do is subtract from the maxWidth, the size of the bubble + the alphabetItem’s width. If we just used maxWidth the bubble would have been drawn out of the screen. If we subtracted just the size of the bubble it be at the edge of the screen, and finally subtracting both bubbleSize and alphabetItemWidth, we can have the bubble just left of the alphabet characters!

For the bubbleOffsetY value, we passed the relative y offset drag in the alphabet container + the distance the container was from the top of the screen. Now let’s say we want the bubble to be center aligned where the user’s finger is like in the gif above. With only the bubbleOffsetY, the bubble would be drawn from y to y + the height of bubble — which draws the bubble at and below the center of the finger. To get it centered, we can divide the bubbleSize by 2 and subtract it from the bubbleOffsetY .

And there we just built the scrolling bubble in Compose!

Phew! I hope you learned a lot about this! And yes, I still didn’t talk about how to show the character in the scrolling bubble or even scrolling to a certain position in the list when a user’s finger is on a character… I’d urge that if you followed along, attempt to build this yourself & if you get stuck I do have a repository demoing these features.

Happy learning & coding!

--

--

Keith Abdulla

android engineer who loves to learn and teach. currently at square!