Build apps faster with our new App builder! Check it out →

SwipeToReveal

Android

Component in Wear Material 3 Compose

[SwipeToReveal] Material composable. This adds the option to configure up to two additional actions on a Composable: a mandatory [SwipeToRevealScope.primaryAction] and an optional [SwipeToRevealScope.secondaryAction]. These actions are initially hidden and revealed only when the [content] is swiped. These additional actions can be triggered by clicking on them after they are revealed. [SwipeToRevealScope.primaryAction] will be triggered on full swipe of the [content].

For actions like "Delete", consider adding [SwipeToRevealScope.undoPrimaryAction] (displayed when the [SwipeToRevealScope.primaryAction] is activated). Adding undo composables allow users to undo the action that they just performed.

[SwipeToReveal] composable adds the [CustomAccessibilityAction]s using the labels from primary and secondary actions.

Example of [SwipeToReveal] with primary and secondary actions

Last updated:

Installation

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

Overloads

@Composable
fun SwipeToReveal(
    actions: SwipeToRevealScope.() -> Unit,
    modifier: Modifier = Modifier,
    revealState: RevealState = rememberRevealState(anchors = SwipeToRevealDefaults.anchors()),
    actionButtonHeight: Dp = SwipeToRevealDefaults.SmallActionButtonHeight,
    gestureInclusion: GestureInclusion =
        if (revealState.hasBidirectionalAnchors()) {
            bidirectionalGestureInclusion()
        } else {
            gestureInclusion()
        },
    content: @Composable () -> Unit,
)

Parameters

namedescription
actionsActions of the [SwipeToReveal] composable, such as [SwipeToRevealScope.primaryAction]. [actions] should always include exactly one [SwipeToRevealScope.primaryAction]. [SwipeToRevealScope.secondaryAction], [SwipeToRevealScope.undoPrimaryAction] and [SwipeToRevealScope.undoSecondaryAction] are optional.
modifier[Modifier] to be applied on the composable
revealState[RevealState] of the [SwipeToReveal]
actionButtonHeightDesired height of the revealed action buttons. In case the content is a Button composable, it's suggested to use [SwipeToRevealDefaults.SmallActionButtonHeight], and for a Card composable, it's suggested to use [SwipeToRevealDefaults.LargeActionButtonHeight].
gestureInclusionProvides fine-grained control so that touch gestures can be excluded when they start in a certain region. An instance of [GestureInclusion] can be passed in here which will determine via [GestureInclusion.ignoreGestureStart] whether the gesture should proceed or not. By default, [gestureInclusion] allows gestures everywhere for when [revealState] contains anchors for both directions (see [bidirectionalGestureInclusion]). If it doesn't, then it allows gestures everywhere, except a zone on the left edge, which is used for swipe-to-dismiss (see [gestureInclusion]).
contentThe content that will be initially displayed over the other actions provided.

Code Examples

SwipeToRevealSample

@Composable
fun SwipeToRevealSample() {
    SwipeToReveal(
        // Use the double action anchor width when revealing two actions
        revealState =
            rememberRevealState(
                anchors =
                    SwipeToRevealDefaults.anchors(
                        anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth,
                    )
            ),
        actions = {
            primaryAction(
                onClick = { /* This block is called when the primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                text = { Text("Delete") }
            )
            secondaryAction(
                onClick = { /* This block is called when the secondary action is executed. */ },
                icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "Options") }
            )
            undoPrimaryAction(
                onClick = { /* This block is called when the undo primary action is executed. */ },
                text = { Text("Undo Delete") },
            )
        }
    ) {
        Button(
            modifier =
                Modifier.fillMaxWidth().semantics {
                    // Use custom actions to make the primary and secondary actions accessible
                    customActions =
                        listOf(
                            CustomAccessibilityAction("Delete") {
                                /* Add the primary action click handler here */
                                true
                            },
                            CustomAccessibilityAction("Options") {
                                /* Add the secondary click handler here */
                                true
                            }
                        )
                },
            onClick = {}
        ) {
            Text("This Button has two actions", modifier = Modifier.fillMaxSize())
        }
    }
}

SwipeToRevealSingleActionCardSample

@Composable
fun SwipeToRevealSingleActionCardSample() {
    SwipeToReveal(
        actionButtonHeight = SwipeToRevealDefaults.LargeActionButtonHeight,
        actions = {
            primaryAction(
                onClick = { /* This block is called when the primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                text = { Text("Delete") }
            )
            undoPrimaryAction(
                onClick = { /* This block is called when the undo primary action is executed. */ },
                text = { Text("Undo Delete") },
            )
        }
    ) {
        Card(
            modifier =
                Modifier.fillMaxWidth().semantics {
                    // Use custom actions to make the primary action accessible
                    customActions =
                        listOf(
                            CustomAccessibilityAction("Delete") {
                                /* Add the primary action click handler here */
                                true
                            },
                        )
                },
            onClick = {}
        ) {
            Text(
                "This Card has one action, and the revealed button is taller",
                modifier = Modifier.fillMaxSize()
            )
        }
    }
}

SwipeToRevealNonAnchoredSample

@Composable
fun SwipeToRevealNonAnchoredSample() {
    SwipeToReveal(
        revealState =
            rememberRevealState(
                anchors = SwipeToRevealDefaults.anchors(useAnchoredActions = false)
            ),
        actions = {
            primaryAction(
                onClick = { /* This block is called when the primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                text = { Text("Delete") }
            )
            undoPrimaryAction(
                onClick = { /* This block is called when the undo primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Refresh, contentDescription = "Undo") },
                text = { Text("Undo") },
            )
        }
    ) {
        Button(
            modifier =
                Modifier.fillMaxWidth().semantics {
                    // Use custom actions to make the primary action accessible
                    customActions =
                        listOf(
                            CustomAccessibilityAction("Delete") {
                                /* Add the primary action click handler here */
                                true
                            },
                        )
                },
            onClick = {}
        ) {
            Text("Swipe to execute the primary action.", modifier = Modifier.fillMaxSize())
        }
    }
}

SwipeToRevealWithTransformingLazyColumnSample

@Preview
@Composable
fun SwipeToRevealWithTransformingLazyColumnSample() {
    val transformationSpec = rememberResponsiveTransformationSpec()
    val tlcState = rememberTransformingLazyColumnState()

    TransformingLazyColumn(
        state = tlcState,
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
        modifier = Modifier.background(Color.Black)
    ) {
        items(count = 100) { index ->
            val revealState =
                rememberRevealState(
                    anchors =
                        SwipeToRevealDefaults.anchors(
                            anchorWidth = SwipeToRevealDefaults.DoubleActionAnchorWidth,
                        )
                )

            // SwipeToReveal is covered on scroll.
            LaunchedEffect(tlcState.isScrollInProgress) {
                if (
                    tlcState.isScrollInProgress && revealState.currentValue != RevealValue.Covered
                ) {
                    revealState.animateTo(targetValue = RevealValue.Covered)
                }
            }

            SwipeToReveal(
                revealState = revealState,
                modifier =
                    Modifier.transformedHeight(transformationSpec::getTransformedHeight)
                        .graphicsLayer {
                            with(transformationSpec) {
                                applyContainerTransformation(scrollProgress)
                            }
                            // Is needed to disable clipping.
                            compositingStrategy = CompositingStrategy.ModulateAlpha
                            clip = false
                        },
                actions = {
                    primaryAction(
                        onClick = { /* Called when the primary action is executed. */ },
                        icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                        text = { Text("Delete") }
                    )
                }
            ) {
                TransformExclusion {
                    TitleCard(
                        onClick = {},
                        title = { Text("Message #$index") },
                        subtitle = { Text("Body of the message") },
                        modifier =
                            Modifier.semantics {
                                // Use custom actions to make the primary action accessible
                                customActions =
                                    listOf(
                                        CustomAccessibilityAction("Delete") {
                                            /* Add the primary action click handler here */
                                            true
                                        },
                                    )
                            }
                    )
                }
            }
        }
    }
}
by @alexstyl