DEV Community

Shiva Thapa
Shiva Thapa

Posted on

Apply SwipeToDismissBox in Android Jetpack Compose

Here, let’s talk about the SwipeToDismissBox Composable which is more compose way to apply swipe to dismiss.

I hope you have seen the swipe feature in the Google’s Gmail app. It is a very handy feature which saves a lot of taps (clicks). Today, we will be implementing something like that using SwipeToDismissBox composable.

Swipe Representation

SwipeToDismissBox Composable

Firstly, I will introduce you to the different parameters that SwipeToDismissBox composable takes,

@Composable
@ExperimentalMaterial3Api
fun SwipeToDismissBox(
    state: SwipeToDismissBoxState,
    backgroundContent: @Composable RowScope.() -> Unit,
    modifier: Modifier = Modifier,
    enableDismissFromStartToEnd: Boolean = true,
    enableDismissFromEndToStart: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {...}
Enter fullscreen mode Exit fullscreen mode
  • state: The state of this component.
  • backgroundContent: A composable that is stacked behind the main content and is visible when the main content is swiped or is being swiped.
  • enableDismissFromStartToEnd: Whether SwipeToDismissBox can be dismissed from start(left) to end(right).
  • enableDismissFromEndToStart: Whether SwipeToDismissBox can be dismissed from end(right) to start(left).
  • content: Our main content that can be dismissed.

SwipeToDismissBoxState

To achieve the behavior we want, we really need to play with SwipeToDismissBoxState. Thanks to developers behind this, we have very little to no things to worry about.

The rememberSwipeToDismissBoxState helps us remember and control the different SwipeToDismissBoxState values.

@Composable
@ExperimentalMaterial3Api
fun rememberSwipeToDismissBoxState(
    initialValue: SwipeToDismissBoxValue = SwipeToDismissBoxValue.Settled,
    confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
    positionalThreshold: (totalDistance: Float) -> Float =
        SwipeToDismissBoxDefaults.positionalThreshold,
): SwipeToDismissBoxState {...}

Enter fullscreen mode Exit fullscreen mode

Peeking into the parameters,

  • initialValue: It is the initial value of the state.
  • confirmValueChange: Optional callback for confirming or overriding a pending state change.
  • positionalThreshold: The distance from the start of a swipe where the target state is calculated. It’s added or subtracted from the origin offset based on swipe direction and is always a positive value.

Let’s understand its use with an example.

I have created a SwipeBox composable function to isolate it from other composables and make it reusable (in my case).

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeBox(
    modifier: Modifier = Modifier,
    onDelete: () -> Unit,
    onEdit: () -> Unit,
    content: @Composable () -> Unit
) {
    val swipeState = rememberSwipeToDismissBoxState()

    lateinit var icon: ImageVector
    lateinit var alignment: Alignment
    val color: Color

    when (swipeState.dismissDirection) {
        SwipeToDismissBoxValue.EndToStart -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            icon = Icons.Outlined.Edit
            alignment = Alignment.CenterStart
            color =
                Color.Green.copy(alpha = 0.3f)

        SwipeToDismissBoxValue.Settled -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }
    }
...
Enter fullscreen mode Exit fullscreen mode

Here, I initialized some variables to store the icon, alignment property, and color which is assigned based on the dissmissDirection.

state.dismissDirection gives us the direction in which the composable is being dismissed. Using this we change the background of the SwipeToDismissBox for different actions i.e. on swipe left, and on swipe right.

To understand this more clearly, there are three directions (dragAnchors to be more specific) for dismissing by swipe. They are, StartToEnd, EndToStart, and Settled.

So, when the dismissDirection is in progress of any of three position we can call the composable that we want in the background. In our case, we assign the corresponding values for the icons, alignment property, and color, and then these properties are used in the backgroundContent below:

(You can directly call the composables unlike this case.)

...
SwipeToDismissBox(
        modifier = modifier.animateContentSize(),
        state = swipeState,
        backgroundContent = {
            Box(
                contentAlignment = alignment,
                modifier = Modifier
                    .fillMaxSize()
                    .background(color)
            ) {
                Icon(
                    modifier = Modifier.minimumInteractiveComponentSize(),
                    imageVector = icon, contentDescription = null
                )
            }
        }
    ) {
        content()
    }
...
Enter fullscreen mode Exit fullscreen mode

Here, we set up the SwipeToDismissBox composable. I passed swipeState as state that I initialized before, and backgroundContent which is the composable that we want behind the main content. content() composable is the main content of our SwipeToDismissBox that we’ll get as an argument in our SwipeBox function.

Now, we would like to execute certain actions if the swipeState changes its positional value i.e. when the swipe is completed. In our case we’ll apply the onDelete() and onEdit() actions on dismissFromEndToStart and dismissFromStartToEnd respectively. We do this using the state’s .currentValue property which gives us the current state value.

...
    when (swipeState.currentValue) {
        SwipeToDismissBoxValue.EndToStart -> {
            onDelete()
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            LaunchedEffect(swipeState) {
                onEdit()
                swipeState.snapTo(SwipeToDismissBoxValue.Settled)
            }
        }

        SwipeToDismissBoxValue.Settled -> {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As the code above speaks for itself, swipeState.currentValue property have three states i.e. EndToStart, StartToEnd, and Settled. When the swipeState is in any of the three state we can apply respective actions on them.

Here, when the EndToStart swipe completes I want to apply onDelete() action. Similarly, when the StartToEnd swipe completes I want to apply onEdit() action, and then set the state to the Settled position using snapTo function which takes the targetValue as an argument.

That’s it, our desired behavior has been achieved. This is just one way of doing it in countless others to achieve this behavior. There is always better way to do it.

Adding with it, there are many things to play with state such as:

  • progress() to get the progress status i.e. start 0f..1f end
  • targetValue() to get the closest state.
  • reset() to reset the component to default position.
  • dismiss() to dismiss the component in the given direction.

Complete Implementation

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DemoSwipeToDismiss(
    modifier: Modifier = Modifier
) {
    var myList by remember {
        mutableStateOf((1..3).toList())
    }

    LazyColumn(modifier = modifier) {
        items(myList) { item ->
            SwipeBox(
                onDelete = {
                    // Just for Example. Is not optimal!
                    myList = myList.toMutableList().also { it.remove(item) }
                },
                onEdit = { },
                modifier = Modifier.animateItemPlacement()
            ) {
                ListItem(headlineContent = { Text(text = "Headline text $item") },
                    supportingContent = { Text(text = "Supporting text $item") },
                    leadingContent = {
                        Icon(
                            imageVector = Icons.Outlined.AccountBox,
                            contentDescription = null
                        )
                    }
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeBox(
    modifier: Modifier = Modifier,
    onDelete: () -> Unit,
    onEdit: () -> Unit,
    content: @Composable () -> Unit
) {
    val swipeState = rememberSwipeToDismissBoxState()

    lateinit var icon: ImageVector
    lateinit var alignment: Alignment
    val color: Color

    when (swipeState.dismissDirection) {
        SwipeToDismissBoxValue.EndToStart -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            icon = Icons.Outlined.Edit
            alignment = Alignment.CenterStart
            color =
                Color.Green.copy(alpha = 0.3f) // You can generate theme for successContainer in themeBuilder
        }

        SwipeToDismissBoxValue.Settled -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }
    }

    SwipeToDismissBox(
        modifier = modifier.animateContentSize(),
        state = swipeState,
        backgroundContent = {
            Box(
                contentAlignment = alignment,
                modifier = Modifier
                    .fillMaxSize()
                    .background(color)
            ) {
                Icon(
                    modifier = Modifier.minimumInteractiveComponentSize(),
                    imageVector = icon, contentDescription = null
                )
            }
        }
    ) {
        content()
    }

    when (swipeState.currentValue) {
        SwipeToDismissBoxValue.EndToStart -> {
            onDelete()
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            LaunchedEffect(swipeState) {
                onEdit()
                swipeState.snapTo(SwipeToDismissBoxValue.Settled)
            }
        }

        SwipeToDismissBoxValue.Settled -> {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)