Bolt UIX
Bolt UIX

Reputation: 7022

How to enable Compose snackbar's swipe-to-dismiss behavior

Simple code below for showing the Compose Snackbar

This code correctly shows the Snackbar, when onClick event occurs.

 val scaffoldState = rememberScaffoldState() // this contains the `SnackbarHostState`
    val coroutineScope = rememberCoroutineScope()

    Scaffold(
        modifier = Modifier,
        scaffoldState = scaffoldState // attaching `scaffoldState` to the `Scaffold`
    ) {
        Button(
            onClick = {
                coroutineScope.launch { // using the `coroutineScope` to `launch` showing the snackbar
                    // taking the `snackbarHostState` from the attached `scaffoldState`
                    val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                        message = "This is your message",
                        actionLabel = "Do something."
                    )
                    when (snackbarResult) {
                        SnackbarResult.Dismissed -> Log.d("SnackbarDemo", "Dismissed")
                        SnackbarResult.ActionPerformed -> Log.d("SnackbarDemo", "Snackbar's button clicked")
                    }
                }
            }
        ) {
            Text(text = "A button that shows a Snackbar")
        }
    }

How to dismiss snackbar on right/left-swipe?

Upvotes: 10

Views: 3925

Answers (2)

Alejandra
Alejandra

Reputation: 882

Material 2 solution

Solved this by using SwipeToDismiss Material component.

@OptIn(ExperimentalMaterialApi::class)
@Composable fun AppSwipeableSnackbarWrapper(
  state: AppSnackbarHostState,
  modifier: Modifier = Modifier,
  dismissSnackbarState: DismissState = rememberDismissState { value ->
    if (value != Default) {
      state.currentSnackbarData?.dismiss()
      true
    } else {
      false
    }
  },
  dismissContent: @Composable RowScope.() -> Unit
) {
  LaunchedEffect(dismissSnackbarState.currentValue) {
    if (dismissSnackbarState.currentValue != Default) {
      dismissSnackbarState.reset()
    }
  }
  SwipeToDismiss(
    modifier = modifier,
    state = dismissSnackbarState,
    background = {},
    dismissContent = dismissContent,
  )
}

And then maybe you can use this wrapper like this or adapt it was you need:

snackbarHost = {
            AppSwipeableSnackbarWrapper(
              state = snackbarHostState,
              dismissContent = { AppSnackbarHost(hostState = snackbarHostState) }
            )
          },

Material 3 solution (Edit January 2024)

Check M3Experiment2 implementation using SwipeToDismissBox.

Upvotes: 2

Phil Dukhov
Phil Dukhov

Reputation: 87894

The SnackbarHost has no such functionality. But you can extend it with the nackbarHost argument.

Also if you want the snackbar to disappear only with a swipe, you probably need to set the duration to Indefinite:

Scaffold(
    modifier = Modifier,
    scaffoldState = scaffoldState,
    snackbarHost = { SwipeableSnackbarHost(it) } // modification 1
) {
    Button(
        onClick = {
            coroutineScope.launch {
                val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                    message = "This is your message",
                    actionLabel = "Do something.",
                    duration = SnackbarDuration.Indefinite,  // modification 2
                )
                when (snackbarResult) {
                    SnackbarResult.Dismissed -> Log.d("SnackbarDemo", "Dismissed")
                    SnackbarResult.ActionPerformed -> Log.d(
                        "SnackbarDemo",
                        "Snackbar's button clicked"
                    )
                }
            }
        }
    ) {
        Text(text = "A button that shows a Snackbar")
    }
}

SwipeableSnackbarHost inspired by this answer

enum class SwipeDirection {
    Left,
    Initial,
    Right,
}

@Composable
fun SwipeableSnackbarHost(hostState: SnackbarHostState) {
    if (hostState.currentSnackbarData == null) { return }
    var size by remember { mutableStateOf(Size.Zero) }
    val swipeableState = rememberSwipeableState(SwipeDirection.Initial)
    val width = remember(size) {
        if (size.width == 0f) {
            1f
        } else {
            size.width
        }
    }
    if (swipeableState.isAnimationRunning) {
        DisposableEffect(Unit) {
            onDispose {
                when (swipeableState.currentValue) {
                    SwipeDirection.Right,
                    SwipeDirection.Left -> {
                        hostState.currentSnackbarData?.dismiss()
                    }
                    else -> {
                        return@onDispose
                    }
                }
            }
        }
    }
    val offset = with(LocalDensity.current) {
        swipeableState.offset.value.toDp()
    }
    SnackbarHost(
        hostState,
        snackbar = { snackbarData ->
            Snackbar(
                snackbarData,
                modifier = Modifier.offset(x = offset)
            )
        },
        modifier = Modifier
            .onSizeChanged { size = Size(it.width.toFloat(), it.height.toFloat()) }
            .swipeable(
                state = swipeableState,
                anchors = mapOf(
                    -width to SwipeDirection.Left,
                    0f to SwipeDirection.Initial,
                    width to SwipeDirection.Right,
                ),
                thresholds = { _, _ -> FractionalThreshold(0.3f) },
                orientation = Orientation.Horizontal
            )
    )
}

Upvotes: 10

Related Questions