

Swipe to reveal in Jetpack Compose
Swipe to dismiss is really easy to implement in compose, including item removal animation by using a combination of SwipeToDismiss & AnimatedVisibility composables.
SwipeToDismiss doesn’t allow us to stop the dragging motion midway though, so let’s take a look how we can achieve the following by detecting horizontal drag gestures on any composable.
Step 1: Creating a data source
Those who’ve read expandable lists in jetpack compose can skip this step, since data source is almost the same.
First we define our model to hold the card related info:
data class CardModel(val id: Int, val title: String)
We’ll be using AAC viewModel example here, but feel free to use any “controller” abstraction you prefer.
This class serves 5 purposes:
- Holds list of cards using MutableStateFlow in _cards field, and exposes a StateFlow to observers via cards field.
- Holds list of “revealed” card ids in _revealedCardIdsList, and exposes them to observers via revealedCardIdsList field.
- Provides list of cards using getFakeData() function. We need a coroutine here, to emit the testList into _cards.
- Has onItemExpanded() to mark cards as revealed, by adding tapped card id to _revealedCardIdsList, and notify observers about this change by mutating the state of _revealedCardIdsList.
- Has onItemCollapsed() to remove revealed card id from _revealedCardIdsList, and notify observers about this change by mutating the state of _revealedCardIdsList.
Step 2: CardsScreen composable
We are observing the viewModel.cards containing our list of cards, and viewModel.expandedCardIds with the help of .collectAsStateWithLifecycle(). UI is quite simple here, each list item is represented with a Box that contains our hidden action icons & the draggable card.
ActionsRow — row with 3 icons, exposes callbacks from each icon tap: onDelete, onEdit, onFavorite.
DraggableCard — our custom card composable that exposes onExpand & onCollapse callbacks.
- we use isRevealed field to apply initial state of the card.
- cardOffset is the amount of horizontal offset of the card when it’s in a “revealed” state. This number should be equal to the width of content under the card. In our case it’s width of 3 icons.
Step 3: DraggableCard composable (short version)
val offsetX by remember { mutableStateOf(0f) }
offsetX — will hold our real time provided horizontal offset value.
val transitionState = remember {
MutableTransitionState(isRevealed).apply {
targetState = !isRevealed
}
}
We declare MutableTransitionState, and put it into our transition composable. Our initialState will depend on whether we have this card id in our revealedCardIds, and targetState will be a reversed initialState, since we only have 2 states.
val offsetTransition by transition.animateFloat(
label = "cardOffsetTransition",
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
targetValueByState = {
if (isRevealed) cardOffset - offsetX else -offsetX },)
The offsetTransition helps us adjust the placement of the card, providing us with the “snap” effect.
.offset { IntOffset((offsetX + offsetTransition).roundToInt(), 0) }
We are combining real time updates from offsetX provided by the detectHorizontalDragGestures, and updates caused by our offsetTransition and passing them to to the offset modifier.
- newValue.x is calculated from the drag amount caused by user.
- newValue.x ≥ 10 allows us to snap the card to revealed state, without forcing user to drag it further. This value can be whatever you find pleasing, eg: the middle of the hidden content. Any drag amount from the revealed state will trigger the snap to hidden state.
Update: there is also a simpler way in the repository.
This example uses compose version 1.3.0-alpha03 and will be periodically updated, to reflect the latest compose version. Feel free to contact me if you know how this approach might be simplified or enhanced.
Full example can be found here: