Blog Infos
Author
Published
Topics
, , , ,
Published

Swipe gestures provide a natural way to interact with elements in an app, adding intuitive controls for actions like dismissing items or revealing options. Jetpack Compose makes it easy to implement in various ways. With recent updates of the Compose libraries, new APIs make swipe-based interactions simpler and more maintainable.

In this article, we’ll explore how to implement the SwipeToDismiss and SwipeToReveal functionality and customize them for various use cases, empowering you to create dynamic, responsive UIs.

Base Implementation with detectHorizontalDragGestures

The first approach for implementing swipe-based interactions is to use detectHorizontalDragGestures, a flexible and foundational solution that allows for full customization. This method enables both SwipeToDismiss and SwipeToReveal functionalities by managing the horizontal drag manually. Below is an example of how to implement this in a composable:

@Composable
fun LibraryBook(
    onClickRead: () -> Unit,
    onClickDelete: () -> Unit
) {
    var offsetX by remember { mutableFloatStateOf(0f) }
    
    Box(
        modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectHorizontalDragGestures { _, dragAmount ->
                    offsetX = (offsetX + dragAmount).coerceIn(-300f, 0f)
                }
            }
    ) {
        // Actions revealed
        Row(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 16.dp),
            horizontalArrangement = Arrangement.End,
            verticalAlignment = Alignment.CenterVertically
        ) {
            IconButton(onClick = onClickDelete) {
                Icon(Icons.Default.Delete, contentDescription = "")
            }
        }

        // Main content
        Box(
            modifier = Modifier
                .offset { IntOffset(offsetX.roundToInt(), 0) }
        ) {
            InternalLibraryBook()
        }
    }
}

In this implementation:

  • We maintain an offsetX state to control the horizontal position of the item as it’s dragged.
  • DetectHorizontalDragGestures handles horizontal dragging, updating offsetX within a specified range to prevent excessive movement.
  • The main content is shifted based on offsetX, revealing the delete action as you swipe.

This approach is straightforward, but it provides the flexibility to expand and customize as needed. If you want to dive deeper into this solution, Philipp Lackner’s video provides an excellent walkthrough. Philipp shares various Compose techniques in his videos, so consider following him for more useful tips and tutorials.

Implementation with SwipeToDismissBox

With recent updates to the Compose libraries, we now have the SwipeToDismissBox, which provides a more structured and controllable approach to swipe-based interactions. This component simplifies the process of implementing dismiss gestures and offers better control over the swipe state. Here’s how it enhances the previous implementation:

@Composable
fun LibraryBook2(
    modifier: Modifier = Modifier,
    onClickRead: () -> Unit,
    onClickDelete: () -> Unit
) {
    val dismissState = rememberSwipeToDismissBoxState(confirmValueChange = {
        when (it) {
            SwipeToDismissBoxValue.EndToStart -> {
                onClickDelete()
                true
            }
            SwipeToDismissBoxValue.StartToEnd -> {
                onClickRead()
                true
            }
            else -> false
        }
    })

    SwipeToDismissBox(
        modifier = modifier,
        state = dismissState,
        backgroundContent = {
            Row(
                modifier = Modifier.fillMaxSize(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                // Read action on swipe from start to end
                AnimatedVisibility(
                    visible = dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd,
                    enter = fadeIn()
                ) {
                    Icon(
                        imageVector = Icons.AutoMirrored.Outlined.MenuBook,
                        contentDescription = "Read"
                    )
                }

                Spacer(modifier = Modifier.weight(1f))

                // Delete action on swipe from end to start
                AnimatedVisibility(
                    visible = dismissState.targetValue == SwipeToDismissBoxValue.EndToStart,
                    enter = fadeIn()
                ) {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "Delete"
                    )
                }
            }
        }
    ) {
        InternalLibraryBook()
    }
}

 

In this updated example:

  • SwipeToDismissBox manages the swipe state internally, which simplifies the swipe handling compared to the detectHorizontalDragGestures approach.
  • The backgroundContent is displayed conditionally based on the swipe direction, using in my case, AnimatedVisibility to smoothly show icons for delete and read actions.
Resetting the Swipe Position

To reset the swipe position after an action is taken, you can leverage LaunchedEffect to monitor dismissState.currentValue and trigger a reset when a swipe is completed:

val dismissState = rememberSwipeToDismissBoxState()

LaunchedEffect(dismissState.currentValue) {
    when (dismissState.currentValue) {
        SwipeToDismissBoxValue.EndToStart -> {
            onClickDelete()
            dismissState.reset()
        }
        SwipeToDismissBoxValue.StartToEnd -> {
            onClickRead()
            dismissState.reset()
        }
        else -> { /* No action needed */ }
    }
}

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

Implementing SwipeToReveal with anchoredDraggable

The SwipeToDismissBox works well for swipe to dismiss interactions, but if we want to implement SwipeToReveal (where swiping reveals options rather than dismissing the item) we need a different approach. I found a powerful alternative with the anchoredDraggable API, as it allows us to define anchor points where specific actions can be triggered, making it ideal for reveal-based interactions.

Here’s the example of implementing SwipeToReveal with anchoredDraggable:

enum class SwipeToRevealValue { Read, Resting, Delete }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryBook3(
    onClickRead: () -> Unit,
    onClickDelete: () -> Unit
) {
    val density = LocalDensity.current
    val decayAnimationSpec = rememberSplineBasedDecay<Float>()
    val dragState = remember {
        val actionOffset = with(density) { 100.dp.toPx() }
        AnchoredDraggableState(
            initialValue = SwipeToRevealValue.Resting,
            anchors = DraggableAnchors {
                SwipeToRevealValue.Read at -actionOffset
                SwipeToRevealValue.Resting at 0f
                SwipeToRevealValue.Delete at actionOffset
            },
            positionalThreshold = { distance -> distance * 0.5f },
            velocityThreshold = { with(density) { 100.dp.toPx() } },
            snapAnimationSpec = tween(),
            decayAnimationSpec = decayAnimationSpec,
        )
    }

    val overScrollEffect = ScrollableDefaults.overscrollEffect()

    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        // Main content that moves with the swipe
        Box(
            modifier = Modifier
                .anchoredDraggable(
                    dragState,
                    Orientation.Horizontal,
                    overscrollEffect = overScrollEffect
                )
                .overscroll(overScrollEffect)
                .offset {
                    IntOffset(
                        x = dragState.requireOffset().roundToInt(),
                        y = 0
                    )
                }
        ) {
            InternalLibraryBook()
        }

        // actions for "Read" and "Delete"
        Row(
            modifier = Modifier.matchParentSize(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            // Read Action
            AnimatedVisibility(
                visible = dragState.currentValue == SwipeToRevealValue.Read,
                enter = slideInHorizontally(animationSpec = tween()) { it },
                exit = slideOutHorizontally(animationSpec = tween()) { it }
            ) {
                IconButton(onClick = onClickRead) {
                    Icon(
                        imageVector = Icons.AutoMirrored.Outlined.MenuBook,
                        contentDescription = "Read"
                    )
                }
            }

            Spacer(modifier = Modifier.weight(1f))

            // Delete Action
            AnimatedVisibility(
                visible = dragState.currentValue == SwipeToRevealValue.Delete,
                enter = slideInHorizontally(animationSpec = tween()) { -it },
                exit = slideOutHorizontally(animationSpec = tween()) { -it }
            ) {
                IconButton(onClick = onClickDelete) {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "Delete"
                    )
                }
            }
        }
    }
}

 

In this setup:

  • AnchoredDraggableState allows us to set specific anchor points for different actions. Here, swiping left reveals the delete option, while swiping right reveals the read option.
  • AnimatedVisibility and slideInHorizontally are used to animate the icons as they are revealed or hidden, creating a smooth interaction.

This approach work well also in the case of the swipe to dismiss interactions. In this case we need to add a LaunchedEffect to call our callbacks at the right moment:

LaunchedEffect(dragState) {
    snapshotFlow { dragState.settledValue }
        .collectLatest {
            when (it) {
                SwipeToRevealValue.Read -> onClickRead()
                SwipeToRevealValue.Delete -> onClickDelete()
                else -> {}
            }
            delay(30)
            dragState.animateTo(SwipeToRevealValue.Resting)
        }
}

 

The LaunchedEffect triggers the appropriate action based on the settled value, then resets the swipe position to maintain a clean UI state after each swipe.

Conclusion

In this article, we’ve explored three powerful approaches to implementing swipe-based interactions in Jetpack Compose: detectHorizontalDragGesturesSwipeToDismissBox, and anchoredDraggable.
Each method has its strengths, allowing for a range of customization and control over swipe behaviors.

  • detectHorizontalDragGestures provides a low-level, customizable approach, ideal if you need control over gesture handling.
  • SwipeToDismissBox simplifies the setup for dismissible items with built-in state management, making it a great choice for straightforward swipe-to-dismiss interactions.
  • anchoredDraggable offers precise control over anchored states, making it well-suited for swipe functionalities.

By choosing the right tool for the job, you can create smooth, intuitive swipe interactions that enhance your app’s UX. Compose continues to evolve, and with these options, you can build flexible and engaging interfaces that feel natural and responsive to users.

If you found this article interesting, feel free to follow me for more insightful content on Android development and Jetpack Compose. I publish new articles almost every week. Don’t hesitate to share your comments or reach out to me on LinkedIn if you prefer.

Have a great day!

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu