SwipeToDismiss

Composable Component

A composable that can be dismissed by swiping left or right.

Common
@Composable
@ExperimentalMaterialApi
fun SwipeToDismiss(
    state: DismissState,
    modifier: Modifier = Modifier,
    directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
    dismissThresholds: (DismissDirection) -> ThresholdConfig = {
        FixedThreshold(DISMISS_THRESHOLD)
    },
    background: @Composable RowScope.() -> Unit,
    dismissContent: @Composable RowScope.() -> Unit,
) =
    BoxWithConstraints(modifier) {
        val width = constraints.maxWidth.toFloat()
        val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl

        val anchors = mutableMapOf(0f to Default)
        if (StartToEnd in directions) anchors += width to DismissedToEnd
        if (EndToStart in directions) anchors += -width to DismissedToStart

        val thresholds = { from: DismissValue, to: DismissValue ->
            dismissThresholds(getDismissDirection(from, to)!!)
        }
        val minFactor =
            if (EndToStart in directions) StandardResistanceFactor else StiffResistanceFactor
        val maxFactor =
            if (StartToEnd in directions) StandardResistanceFactor else StiffResistanceFactor
        Box(
            Modifier.swipeable(
                state = state,
                anchors = anchors,
                thresholds = thresholds,
                orientation = Orientation.Horizontal,
                enabled = state.currentValue == Default,
                reverseDirection = isRtl,
                resistance =
                    ResistanceConfig(
                        basis = width,
                        factorAtMin = minFactor,
                        factorAtMax = maxFactor,
                    ),
            )
        ) {
            Row(content = background, modifier = Modifier.matchParentSize())
            Row(
                content = dismissContent,
                modifier = Modifier.offset { IntOffset(state.offset.value.roundToInt(), 0) },
            )
        }
    }

Parameters

stateThe state of this component.
modifierOptional Modifier for this component.
directionsThe set of directions in which the component can be dismissed.
dismissThresholdsThe thresholds the item needs to be swiped in order to be dismissed.
backgroundA composable that is stacked behind the content and is exposed when the content is swiped. You can/should use the state to have different backgrounds on each side.
dismissContentThe content that can be dismissed.

Code Examples

SwipeToDismissListItems

@Composable
@OptIn(ExperimentalMaterialApi::class)
fun SwipeToDismissListItems() {
    // This is an example of a list of dismissible items, similar to what you would see in an
    // email app. Swiping left reveals a 'delete' icon and swiping right reveals a 'done' icon.
    // The background will start as grey, but once the dismiss threshold is reached, the colour
    // will animate to red if you're swiping left or green if you're swiping right. When you let
    // go, the item will animate out of the way if you're swiping left (like deleting an email) or
    // back to its default position if you're swiping right (like marking an email as read/unread).
    LazyColumn {
        items(items) { item ->
            var unread by remember { mutableStateOf(false) }
            val dismissState =
                rememberDismissState(
                    confirmStateChange = {
                        if (it == DismissedToEnd) unread = !unread
                        it != DismissedToEnd
                    }
                )
            SwipeToDismiss(
                state = dismissState,
                modifier = Modifier.padding(vertical = 4.dp),
                directions = setOf(StartToEnd, EndToStart),
                background = {
                    val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
                    val color by
                        animateColorAsState(
                            when (dismissState.targetValue) {
                                Default -> Color.LightGray
                                DismissedToEnd -> Color.Green
                                DismissedToStart -> Color.Red
                            }
                        )
                    val alignment =
                        when (direction) {
                            StartToEnd -> Alignment.CenterStart
                            EndToStart -> Alignment.CenterEnd
                        }
                    val icon =
                        when (direction) {
                            StartToEnd -> Icons.Default.Done
                            EndToStart -> Icons.Default.Delete
                        }
                    val scale by
                        animateFloatAsState(if (dismissState.targetValue == Default) 0.75f else 1f)
                    Box(
                        Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
                        contentAlignment = alignment,
                    ) {
                        Icon(
                            icon,
                            contentDescription = "Localized description",
                            modifier = Modifier.scale(scale),
                        )
                    }
                },
                dismissContent = {
                    Card(
                        elevation =
                            animateDpAsState(
                                    if (dismissState.dismissDirection != null) 4.dp else 0.dp
                                )
                                .value
                    ) {
                        ListItem(
                            text = {
                                Text(item, fontWeight = if (unread) FontWeight.Bold else null)
                            },
                            secondaryText = { Text("Swipe me left or right!") },
                        )
                    }
                },
            )
        }
    }
}