SwipeToReveal

Functions summary

Unit
@Composable
SwipeToReveal(
    primaryAction: @Composable SwipeToRevealScope.() -> Unit,
    onSwipePrimaryAction: () -> Unit,
    modifier: Modifier,
    secondaryAction: (@Composable SwipeToRevealScope.() -> Unit)?,
    undoPrimaryAction: (@Composable SwipeToRevealScope.() -> Unit)?,
    undoSecondaryAction: (@Composable SwipeToRevealScope.() -> Unit)?,
    revealState: RevealState,
    revealDirection: RevealDirection,
    hasPartiallyRevealedState: Boolean,
    gestureInclusion: GestureInclusion,
    content: @Composable () -> Unit
)

SwipeToReveal Material composable.

Functions

@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
): Unit

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 Cards, 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 hasPartiallyRevealedState = true, RevealState should be reset to RevealValue.Covered by the caller when scrolling occurs. This is because the revealed actions are vertically centered on the visible part of the content when the actions are revealed and, if the RevealState is not reset to RevealValue.Covered, actions for tall items would appear off-center when the list is scrolled. See the code samples for examples.

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

Example of SwipeToReveal with primary and secondary actions and custom accessibility actions:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.SwipeToReveal
import androidx.wear.compose.material3.Text

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())
    }
}

Example of SwipeToReveal with a Card composable, it reveals a taller button:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Card
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.SwipeToReveal
import androidx.wear.compose.material3.SwipeToRevealDefaults
import androidx.wear.compose.material3.Text

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(),
        )
    }
}

Example of SwipeToReveal with a androidx.wear.compose.foundation.lazy.TransformingLazyColumn, including resetting the RevealState to RevealValue.Covered when scrolling:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.items
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Card
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.RevealValue
import androidx.wear.compose.material3.SwipeToReveal
import androidx.wear.compose.material3.SwipeToRevealDefaults
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TitleCard
import androidx.wear.compose.material3.lazy.rememberTransformationSpec
import androidx.wear.compose.material3.lazy.transformedHeight
import androidx.wear.compose.material3.rememberRevealState

val transformationSpec = rememberTransformationSpec()
val tlcState = rememberTransformingLazyColumnState()
val coroutineScope = rememberCoroutineScope()
val messages = remember {
    mutableStateListOf<String>().apply {
        for (i in 1..100) {
            add("Message #${i}")
        }
    }
}

TransformingLazyColumn(
    state = tlcState,
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
    modifier = Modifier.background(Color.Black),
) {
    items(items = messages, key = { it }) { message ->
        val revealState = rememberRevealState()

        // SwipeToReveal should be reset to covered when scrolling occurs.
        LaunchedEffect(tlcState.isScrollInProgress) {
            if (
                tlcState.isScrollInProgress && revealState.currentValue != RevealValue.Covered
            ) {
                coroutineScope.launch {
                    revealState.animateTo(targetValue = RevealValue.Covered)
                }
            }
        }

        SwipeToReveal(
            primaryAction = {
                PrimaryActionButton(
                    onClick = { messages.remove(message) },
                    icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                    text = { Text("Delete") },
                    modifier = Modifier.height(SwipeToRevealDefaults.LargeActionButtonHeight),
                )
            },
            revealState = revealState,
            onSwipePrimaryAction = { messages.remove(message) },
            modifier =
                Modifier.transformedHeight(this@items, transformationSpec)
                    .animateItem()
                    .graphicsLayer {
                        with(transformationSpec) {
                            applyContainerTransformation(scrollProgress)
                        }
                        // Is needed to disable clipping.
                        compositingStrategy = CompositingStrategy.ModulateAlpha
                        clip = false
                    },
        ) {
            TitleCard(
                onClick = {},
                title = { Text(message) },
                subtitle = { Text("Subtitle") },
                modifier =
                    Modifier.semantics {
                        // Use custom actions to make the primary action accessible
                        customActions =
                            listOf(
                                CustomAccessibilityAction("Delete") {
                                    messages.remove(message)
                                    true
                                }
                            )
                    },
            ) {
                Text("Message body which extends over multiple lines to extend the card")
            }
        }
    }
}

Example of SwipeToReveal with a androidx.wear.compose.foundation.lazy.ScalingLazyColumn, including resetting the RevealState to RevealValue.Covered when scrolling:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.items
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.RevealValue
import androidx.wear.compose.material3.SwipeToReveal
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.rememberRevealState

val slcState = rememberScalingLazyListState()
val coroutineScope = rememberCoroutineScope()
val messages = remember {
    mutableStateListOf<String>().apply {
        for (i in 1..100) {
            add("This Button $i has two actions")
        }
    }
}

ScalingLazyColumn(
    state = slcState,
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
    modifier = Modifier.background(Color.Black),
) {
    items(items = messages, key = { it }) { message ->
        val revealState = rememberRevealState()

        // SwipeToReveal should be reset to covered when scrolling occurs.
        LaunchedEffect(slcState.isScrollInProgress) {
            if (
                slcState.isScrollInProgress && revealState.currentValue != RevealValue.Covered
            ) {
                coroutineScope.launch {
                    revealState.animateTo(targetValue = RevealValue.Covered)
                }
            }
        }

        SwipeToReveal(
            revealState = revealState,
            primaryAction = {
                PrimaryActionButton(
                    onClick = { messages.remove(message) },
                    icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                    text = { Text("Delete") },
                )
            },
            onSwipePrimaryAction = { messages.remove(message) },
            secondaryAction = {
                SecondaryActionButton(
                    onClick = { /* This block is called when the secondary action is executed. */
                    },
                    icon = { Icon(Icons.Outlined.MoreVert, contentDescription = "Options") },
                )
            },
        ) {
            Button(
                modifier =
                    Modifier.fillMaxWidth().semantics {
                        // Use custom actions to make the primary and secondary actions
                        // accessible
                        customActions =
                            listOf(
                                CustomAccessibilityAction("Delete") {
                                    messages.remove(message)
                                    true
                                },
                                CustomAccessibilityAction("Options") {
                                    /* Add the secondary click handler here */
                                    true
                                },
                            )
                    },
                onClick = {},
            ) {
                Text(message, modifier = Modifier.fillMaxSize())
            }
        }
    }
}

Example of SwipeToReveal with a androidx.wear.compose.foundation.lazy.ScalingLazyColumn that only executes the primary action when fully swiped (and does not settle after partially revealing the action) by setting hasPartiallyRevealedState = false (so RevealState does not need to be reset when scrolling):

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.items
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.SwipeToReveal
import androidx.wear.compose.material3.Text

val slcState = rememberScalingLazyListState()
val messages = remember {
    mutableStateListOf<String>().apply {
        for (i in 1..100) {
            add("Message #${i}")
        }
    }
}

ScalingLazyColumn(
    state = slcState,
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
    modifier = Modifier.background(Color.Black),
) {
    items(items = messages, key = { it }) { message ->
        SwipeToReveal(
            hasPartiallyRevealedState = false,
            primaryAction = {
                PrimaryActionButton(
                    onClick = { messages.remove(message) },
                    icon = { Icon(Icons.Outlined.Delete, contentDescription = "Delete") },
                    text = { Text("Delete") },
                )
            },
            onSwipePrimaryAction = { messages.remove(message) },
        ) {
            Button(
                modifier =
                    Modifier.fillMaxWidth().semantics {
                        // Use custom actions to make the primary action accessible
                        customActions =
                            listOf(
                                CustomAccessibilityAction("Delete") {
                                    messages.remove(message)
                                    true
                                }
                            )
                    },
                onClick = {},
            ) {
                Text(message, modifier = Modifier.fillMaxSize())
            }
        }
    }
}
Parameters
primaryAction: @Composable SwipeToRevealScope.() -> Unit

The 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.

onSwipePrimaryAction: () -> Unit

A callback which will be triggered when a full swipe is performed. It is expected that the same callback is given to SwipeToRevealScope.PrimaryActionButtons onClick action. If undoPrimaryAction is provided, that will be displayed after the swipe gesture is completed.

modifier: Modifier = Modifier

Modifier to be applied on the composable.

secondaryAction: (@Composable SwipeToRevealScope.() -> Unit)? = null

Optional 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.

undoPrimaryAction: (@Composable SwipeToRevealScope.() -> Unit)? = null

Optional 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.

undoSecondaryAction: (@Composable SwipeToRevealScope.() -> Unit)? = null

Optional 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 = rememberRevealState()

RevealState of the SwipeToReveal.

revealDirection: RevealDirection = RevealDirection.RightToLeft

The 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.

hasPartiallyRevealedState: Boolean = true

Determines 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).

gestureInclusion: GestureInclusion = if (revealDirection == Bidirectional) { bidirectionalGestureInclusion } else { gestureInclusion(revealState) }

Provides 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).

content: @Composable () -> Unit

The 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.

See also
SwipeToReveal