New Compose Multiplatform components arrived on Composables UICheck it out →

SwipeToDismissBox

Wear Material 3 [SwipeToDismissBox] that handles the swipe-to-dismiss gesture. Takes a single slot for the background (only displayed during the swipe gesture) and the foreground content.

Last updated:

Installation

dependencies {
   implementation("androidx.wear.compose:compose-material3:1.0.0-alpha23")
}

Overloads

@Composable
fun SwipeToDismissBox(
    state: SwipeToDismissBoxState,
    modifier: Modifier = Modifier,
    backgroundScrimColor: Color = MaterialTheme.colorScheme.background,
    contentScrimColor: Color = MaterialTheme.colorScheme.background,
    backgroundKey: Any = SwipeToDismissKeys.Background,
    contentKey: Any = SwipeToDismissKeys.Content,
    userSwipeEnabled: Boolean = true,
    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
)

Parameters

namedescription
stateState containing information about ongoing swipe or animation.
modifier[Modifier] for this component.
backgroundScrimColor[Color] for background scrim.
contentScrimColor[Color] used for the scrim over the content composable during the swipe gesture.
backgroundKey[key] which identifies the content currently composed in the [content] block when isBackground == true. Provide the backgroundKey if your background content will be displayed as a foreground after the swipe animation ends (as is common when [SwipeToDismissBox] is used for the navigation). This allows remembered state to be correctly moved between background and foreground.
contentKey[key] which identifies the content currently composed in the [content] block when isBackground == false. See [backgroundKey].
userSwipeEnabledWhether the swipe gesture is enabled. (e.g. when there is no background screen, set userSwipeEnabled = false)
contentSlot for content, with the isBackground parameter enabling content to be displayed behind the foreground content - the background is normally hidden, is shown behind a scrim during the swipe gesture, and is shown without scrim once the finger passes the swipe-to-dismiss threshold.
@Composable
fun SwipeToDismissBox(
    onDismissed: () -> Unit,
    modifier: Modifier = Modifier,
    state: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
    backgroundScrimColor: Color = MaterialTheme.colorScheme.background,
    contentScrimColor: Color = MaterialTheme.colorScheme.background,
    backgroundKey: Any = SwipeToDismissKeys.Background,
    contentKey: Any = SwipeToDismissKeys.Content,
    userSwipeEnabled: Boolean = true,
    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
)

Parameters

namedescription
onDismissedExecutes when the swipe to dismiss has completed.
modifier[Modifier] for this component.
stateState containing information about ongoing swipe or animation.
backgroundScrimColor[Color] for background scrim.
contentScrimColor[Color] used for the scrim over the content composable during the swipe gesture.
backgroundKey[key] which identifies the content currently composed in the [content] block when isBackground == true. Provide the backgroundKey if your background content will be displayed as a foreground after the swipe animation ends (as is common when [SwipeToDismissBox] is used for the navigation). This allows remembered state to be correctly moved between background and foreground.
contentKey[key] which identifies the content currently composed in the [content] block when isBackground == false. See [backgroundKey].
userSwipeEnabledWhether the swipe gesture is enabled. (e.g. when there is no background screen, set userSwipeEnabled = false)
contentSlot for content, with the isBackground parameter enabling content to be displayed behind the foreground content - the background is normally hidden, is shown behind a scrim during the swipe gesture, and is shown without scrim once the finger passes the swipe-to-dismiss threshold.

Code Examples

StatefulSwipeToDismissBox

@Composable
@Sampled
fun StatefulSwipeToDismissBox() {
    // State for managing a 2-level navigation hierarchy between
    // MainScreen and ItemScreen composables.
    // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation.
    var showMainScreen by remember { mutableStateOf(true) }
    val saveableStateHolder = rememberSaveableStateHolder()

    // Swipe gesture dismisses ItemScreen to return to MainScreen.
    val state = rememberSwipeToDismissBoxState()
    LaunchedEffect(state.currentValue) {
        if (state.currentValue == SwipeToDismissValue.Dismissed) {
            state.snapTo(SwipeToDismissValue.Default)
            showMainScreen = !showMainScreen
        }
    }

    // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind
    // the ItemScreen, otherwise there's no background to show.
    SwipeToDismissBox(
        state = state,
        userSwipeEnabled = !showMainScreen,
        backgroundKey = if (!showMainScreen) "MainKey" else "Background",
        contentKey = if (showMainScreen) "MainKey" else "ItemKey",
    ) { isBackground ->

        if (isBackground || showMainScreen) {
            // Best practice would be to use State Hoisting and leave this composable stateless.
            // Here, we want to support MainScreen being shown from different destinations
            // (either in the foreground or in the background during swiping) - that can be achieved
            // using SaveableStateHolder and rememberSaveable as shown below.
            saveableStateHolder.SaveableStateProvider(
                key = "MainKey",
                content = {
                    // Composable that maintains its own state
                    // and can be shown in foreground or background.
                    val checked = rememberSaveable { mutableStateOf(true) }
                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(horizontal = 8.dp, vertical = 8.dp),
                        verticalArrangement = Arrangement.spacedBy(
                            4.dp, Alignment.CenterVertically
                        ),
                    ) {
                        Row(
                            modifier = Modifier
                                .height(40.dp)
                                .background(
                                    color = MaterialTheme.colorScheme.surfaceContainer,
                                    shape = CircleShape
                                )
                                .padding(horizontal = 12.dp),
                            verticalAlignment = Alignment.CenterVertically,
                        ) {
                            Box(
                                modifier = Modifier.clickable { showMainScreen = false }
                            ) {
                                Text("Item details")
                            }
                            ToggleButton(
                                label = { Text("Checkbox", maxLines = 1) },
                                checked = checked.value,
                                toggleControl = { Checkbox() },
                                onCheckedChange = { checked.value = it },
                            )
                        }
                    }
                }
            )
        } else {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.primary),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center,
            ) {
                Text("Show details here...", color = MaterialTheme.colorScheme.onPrimary)
                Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary)
            }
        }
    }
}

EdgeSwipeForSwipeToDismiss

@Composable
@Sampled
fun EdgeSwipeForSwipeToDismiss(
    navigateBack: () -> Unit
) {
    val state = rememberSwipeToDismissBoxState()

    // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the
    // modifier applies exists within a SwipeToDismissBox which shares the same state.
    SwipeToDismissBox(
        state = state,
        onDismissed = navigateBack
    ) { isBackground ->
        val horizontalScrollState = rememberScrollState(0)
        if (isBackground) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.secondaryContainer)
            )
        } else {
            Box(modifier = Modifier.fillMaxSize()) {
                Text(
                    modifier = Modifier
                        .align(Alignment.Center)
                        .edgeSwipeToDismiss(state)
                        .horizontalScroll(horizontalScrollState),
                    text = "This text can be scrolled horizontally - to dismiss, swipe " +
                        "right from the left edge of the screen (called Edge Swiping)",
                )
            }
        }
    }
}

SimpleSwipeToDismissBox

@Composable
@Sampled
fun SimpleSwipeToDismissBox(
    navigateBack: () -> Unit
) {
    SwipeToDismissBox(
        onDismissed = navigateBack
    ) { isBackground ->
        if (isBackground) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.secondaryContainer)
            )
        } else {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.primary),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center,
            ) {
                Text("Swipe right to dismiss", color = MaterialTheme.colorScheme.onPrimary)
            }
        }
    }
}