Building Google Contacts Screen and its Scrolling Bubble Feature - in Compose
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.
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.
- The main container that’s holding the contacts
- The contact items themselves
- The sidebar
- 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
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
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
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:
verticalAlignmentin 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.
isAlphabeticallyFirstInCharGroupis 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.
- We are using
- 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.
- We introduced
Boxinside the surface to center the text in the middle of the circle, because
Shapedoes 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
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 the
Map , we know
isAlphabeticallyFirstInCharGroup is true.
For our Contact List, I intentionally used
LazyColumn to hold the list of
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
- 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
rememberfunction with the key being the contacts list.
AlphabetScrollerdoesn’t usually take the entire height of the screen and we want to vertically center it on the screen. Therefore, on the
Rowwe’ve added a
verticalAlignmentto do this.
ContactListwould take the entire height if we had enough contacts, but let’s say we have a couple
Contacts then without passing the
ContactListthese contacts would be in the center of the screen.
- Lastly, the
Modifier.weight(1F)passed to the
ContactListenables it to take the remaining width of the screen that the
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
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
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
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
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.
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.
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!
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
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!