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 [primaryAction] and an optional [secondaryAction]. These actions are initially hidden (unless [RevealState] is created with an initial value other than [RevealValue.Covered]) and revealed only when the [content] is swiped - the action buttons can then be clicked. A full swipe of the [content] triggers the [onSwipePrimaryAction] callback, which is expected to match the [primaryAction]'s onClick callback. Custom accessibility actions should always be added to the content using [Modifier.semantics] - examples are shown in the code samples.

Adding undo actions allows users to undo a primary or secondary action that may have been performed inadvertently. The corresponding undo action is displayed when the primary or secondary action is triggered via click or swipe and after [SwipeToReveal] has animated to the revealed state. After the undo action is clicked, [SwipeToReveal] animates back to the [RevealValue.Covered] state and the user can then swipe to reveal if they wish to perform another action.

For destructive actions like "Delete", consider making this the primary action, and providing the [undoPrimaryAction].

When using SwipeToReveal with large content items like [Card]s, it is recommended to set the height of [SwipeToRevealScope.PrimaryActionButton] and [SwipeToRevealScope.SecondaryActionButton] to [SwipeToRevealDefaults.LargeActionButtonHeight] using [Modifier.height] - in other cases, the [Button] displayed by [SwipeToReveal] has height [ButtonDefaults.Height] by default. It is recommended to always use the default undo button height as created by [SwipeToRevealScope.UndoActionButton].

If [revealDirection] is set to [RevealDirection.Bidirectional], the actions revealed on swipe are the same on both sides.

Last updated:

Installation

dependencies {
   implementation("androidx.wear.compose:compose-material3:1.5.0-beta01")
}

Overloads

@Composable
fun SwipeToReveal(
    primaryAction: @Composable SwipeToRevealScope.() -> Unit,
    onSwipePrimaryAction: () -> Unit,
    modifier: Modifier = Modifier,
    secondaryAction: (@Composable SwipeToRevealScope.() -> Unit)? = null,
    undoPrimaryAction: (@Composable SwipeToRevealScope.() -> Unit)? = null,
    undoSecondaryAction: (@Composable SwipeToRevealScope.() -> Unit)? = null,
    revealState: RevealState = rememberRevealState(),
    revealDirection: RevealDirection = RevealDirection.RightToLeft,
    hasPartiallyRevealedState: Boolean = true,
    gestureInclusion: GestureInclusion =
        if (revealDirection == Bidirectional) {
            bidirectionalGestureInclusion
        } else {
            gestureInclusion(revealState)
        },
    content: @Composable () -> Unit,
)

Parameters

namedescription
primaryActionThe primary action of this component. [SwipeToRevealScope.PrimaryActionButton] should be used to create a button for this slot. If [undoPrimaryAction] is provided, the undo button will be displayed after [SwipeToReveal] has animated to the revealed state and the primary action button has been hidden.
onSwipePrimaryActionA callback which will be triggered when a full swipe is performed. It is expected that the same callback is given to [SwipeToRevealScope.PrimaryActionButton]s onClick action. If [undoPrimaryAction] is provided, that will be displayed after the swipe gesture is completed.
modifier[Modifier] to be applied on the composable.
secondaryActionOptional secondary action of this component. [SwipeToRevealScope.SecondaryActionButton] should be used to create a button for this slot. If [undoSecondaryAction] is provided, the undo button will be displayed after [SwipeToReveal] has animated to the revealed state and the secondary action button has been hidden.
undoPrimaryActionOptional undo action for the primary action of this component. [SwipeToRevealScope.UndoActionButton] should be used to create a button for this slot. Displayed after [SwipeToReveal] has animated to the revealed state and the primary action button has been hidden.
undoSecondaryActionOptional undo action for the secondary action of this component, displayed after [SwipeToReveal] has animated to the revealed state and the secondary action button has been hidden. [undoSecondaryAction] is ignored if the secondary action has not been specified. [SwipeToRevealScope.UndoActionButton] should be used to create a button for this slot.
revealState[RevealState] of the [SwipeToReveal].
revealDirectionThe direction from which [SwipeToReveal] can reveal the actions. It is strongly recommended to respect the default value of [RightToLeft] to avoid conflicting with the system-side swipe-to-dismiss gesture.
hasPartiallyRevealedStateDetermines whether the intermediate states [RightRevealing] and [LeftRevealing] are used. These indicate a settled state, where the primary action is partially revealed. By default, partially revealed state is allowed for single actions - set to false to make actions complete when swiped instead. This flag has no effect if a secondary action is provided (when there are two actions, the component always allows the partially revealed states).
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. Custom accessibility actions should always be added to the content using [Modifier.semantics] - examples are shown in the code samples.

Code Examples

SwipeToRevealSample

@Composable
fun SwipeToRevealSample() {
    SwipeToReveal(
        primaryAction = {
            PrimaryActionButton(
                onClick = { /* This block is called when the primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                text = { Text("Delete") }
            )
        },
        onSwipePrimaryAction = { /* This block is called when the full swipe gesture is performed. */
        },
        secondaryAction = {
            SecondaryActionButton(
                onClick = { /* This block is called when the secondary action is executed. */ },
                icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "Options") }
            )
        },
        undoPrimaryAction = {
            UndoActionButton(
                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(
        primaryAction = {
            PrimaryActionButton(
                onClick = { /* This block is called when the primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                text = { Text("Delete") },
                modifier = Modifier.height(SwipeToRevealDefaults.LargeActionButtonHeight)
            )
        },
        onSwipePrimaryAction = { /* This block is called when the full swipe gesture is performed. */
        },
        undoPrimaryAction = {
            UndoActionButton(
                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()
            )
        }
    }
}

SwipeToRevealNoPartiallyRevealedStateSample

@Composable
fun SwipeToRevealNoPartiallyRevealedStateSample() {
    SwipeToReveal(
        primaryAction = {
            PrimaryActionButton(
                onClick = { /* This block is called when the primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                text = { Text("Delete") }
            )
        },
        onSwipePrimaryAction = { /* This block is called when the full swipe gesture is performed. */
        },
        undoPrimaryAction = {
            UndoActionButton(
                onClick = { /* This block is called when the undo primary action is executed. */ },
                icon = { Icon(Icons.Outlined.Refresh, contentDescription = "Undo") },
                text = { Text("Undo") },
            )
        },
        hasPartiallyRevealedState = false
    ) {
        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 = rememberTransformationSpec()
    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()

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

            SwipeToReveal(
                primaryAction = {
                    PrimaryActionButton(
                        onClick = { /* Called when the primary action is executed. */ },
                        icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                        text = { Text("Delete") }
                    )
                },
                revealState = revealState,
                onSwipePrimaryAction = { /* This block is called when the full swipe gesture is performed. */
                },
                modifier =
                    Modifier.transformedHeight(this@items, transformationSpec).graphicsLayer {
                        with(transformationSpec) { applyContainerTransformation(scrollProgress) }
                        // Is needed to disable clipping.
                        compositingStrategy = CompositingStrategy.ModulateAlpha
                        clip = false
                    },
            ) {
                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