Implementing custom hideable bottom sheet in Jetpack Compose
Have you ever spent hours carefully designing your UI in Jetpack Compose just to run into a brick block when the standard bottom sheet components fall short of your unique requirements? If you answered yes, you are undoubtedly not alone. While Jetpack Compose comes with two deliciously simple forms of bottom sheets, there are times when their functionality can feel constrained, leading us to look for more adaptable solutions.
Making a custom solution assumes the role of the hero when the pre-built solutions fall short. In this article, we’ll get deep into creating a custom hideable bottom sheet that is specifically tailored to the user experience of your app. But first, let’s dive into the components we are gonna use.
Intro to the AnchoredDraggable
In order to create the custom bottom sheet we will have to use AnchoredDraggable. AnchoredDraggable is a new Jetpack Compose component that allows us to make content draggable and control it. To have access to it, we need to include the newest alpha version of the Jetpack Compose foundation library:
implementation "androidx.compose.foundation:foundation:1.6.0-alpha06"
Note: At the time of writing this article, 1.6.0-alpha06 was the newest version. Check if there is a newer version and keep in mind that this is the alpha version and some changes might happen.
The first thing that we are gonna need is AnchoredDraggableState. This state holds information about anchoredDraggable modifier that will be used to make composable draggable. With its anchors, it will control how much the item can be dragged and to where. It accepts the following parameters:
- initialValue: T — The initial value of the state and it can be anything, in the current Jetpack Compose bottom sheets, they are enums like: Expanded, HalfExpanded and so on.
- positionalThreshold: (totalDistance: Float) -> Float — Lambda which will be used to calculate the target state or after how much px it should move to the next value of the state.
- velocityThreshold: () -> Float — Lambda which will be used to determine px per second that if passed component should animate to the next state value.
- animationSpec: AnimationSpec<Float> — The default animation that will be used when animating to a new value.
- confirmValueChange: (newValue: T) -> Boolean = { true } — Optional callback invoked to confirm or veto a pending state change.
HideableBottomSheet Implementation
In our case, we are gonna create an enum class HideableBottomSheetValue which will have three values: Hidden, HalfExpanded and Expanded. It looks like this:
For the sake of state-holding and better control, we will create a wrapper that will contain AnchoredDraggableState. We will call it HideableBottomSheetState and it will look like this:
Don’t forget to add @OptIn(ExperimentalFoundationApi::class) as AnchoredDraggableState is still experimental. You can also notice that I created object HideableBottomSheetDefaults which holds default values, it is not that important you can go on without it. It looks like this:
Now let’s add some basic things that we are gonna need and it will just call corresponding fields/functions from the AnchoredDraggableState:
As we know, we will have to remember this state so it is not re-created in every composition, but also we need to rememberSaveable so it survives the activity or process recreation when the user changes the orientation, for example. Because of this, we will have to create custom Saver so that we can save this class. Add Saver companion object function to our state:
And finally, add our rememberHideableBottomSheetState function:
Now is the time to create a layout that will hold both content and the bottom sheet. We will call itHideableBottomSheetScaffold, it will have to be a Box that contains content and another Box wrapper around the bottom sheet content. It needs to accept params like HideableBottomSheetState, bottomSheetContent but also the layout content. Before that, we need to add two things.
The first thing is to add draggableStateFraction field to HideableBottomSheetValue which represents how much of the free space will the current draggable state cover:
The second thing is to add updateAnchors function to the HideableBottomSheetState. This function will calculate the anchors of the AnchoredDraggableState, for each HideableBottomSheetValue it will calculate based on the layout and sheet height how many pixels will the bottom sheet cover. It will be either zero, half of the layout, or full layout height, or if the sheet content height is lower than any of these values then it will cover the sheet content height pixels:
Note: I put the top padding of 32dp, but this is totally my preference. You can put here anything or just have zero padding.
After we added these things, let’s now add HideableBottomSheetScaffold:
First Box that you can see is the box that contains all the content of the scaffold. Then you can see the layout or the screen content and after that the bottom sheet.
You can notice that we call the updateAnchors function when the whole layout or the sheet size is changed. To the outer Box that surrounds the bottom sheet content, we apply .offset modifier which will set the position of the bottom sheet, and the rest of the space is just filled from the bottom. Also, we apply anchoredDraggable which is the main modifier and makes this bottom sheet draggable. anchoredDraggable takes next params:
- state: AnchoredDraggableState<T> — The associated draggable state.
- orientation: Orientation — The orientation of the dragging.
- enabled: Boolean = true — If the dragging is enabled or not.
- reverseDirection: Boolean = false — If the direction is reversed or not.
- interactionSource: MutableInteractionSource? = null — The optional stream of interactions that is passed to the inner draggable.
Now, let’s try it out and see what it looks like and if it works:
Showing and hiding via the button works and also dragging, but you may notice one bug. The sheet is only draggable in the top part, not in the middle. That is because we have a LazyColumn inside the bottom sheet content which is scrollable and all drag/scroll events are consumed by it. We need to create a nestedScroll connection that will drag the bottom sheet until it is fully expanded and then scroll the content.
Let’s create a new nested scroll connection called ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection:
A similar one you can find in the BottomSheetScaffold code. It is internal and we can’t use it so we need to write our own.
Now just add the .nestedScroll(bottomSheetNestedScrollConnection) modifier to the outer bottom sheetBox just under the anchoredDraggable modifier. Now everything works fine, amazing!
Here is the full code:
One more cool thing that can be added is for example onDismiss callback:
LaunchedEffect(bottomSheetState.targetValue) {
if (bottomSheetState.isHidingInProgress()) {
bottomSheetState.onDismiss()
}
}
With this, we finished our bottom sheet, I hope the feeling is good. It was a little bit longer article, but I hope you find it very useful.
All of the source code you can find in my GitHub repo.
Want to Connect?
GitHub
LinkedIn
Twitter
Portfolio website
If you want to learn about the Jetpack Compose, take a look at these articles:
- Jetpack Compose Clean and Encapsulated Composables
- Jetpack Compose Clean Navigation
- Build a Camera Android App in Jetpack Compose Using CameraX
- Implement Bottom Sheet in Jetpack Compose
Jetpack Compose Custom Hideable Bottom Sheet was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.