I'd like my CustomSheet to cover the screen properly and block all interactions with the screen underneath. I thought this was the default behaviour for any Composable that is on top. However, in this case, I can still click the button on the HomeScreen even though the entire screen is covered. Do u have any ideas?
My CustomSheet wrapper:
val visibleState = remember { MutableTransitionState(false) }.apply { targetState = visible } if (visibleState.currentState || visibleState.targetState || !visibleState.isIdle) { Box(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.scrim) ) AnimatedVisibility( modifier = Modifier.align(Alignment.BottomCenter), visibleState = visibleState, enter = slideInVertically( initialOffsetY = { it }, animationSpec = tween(durationMillis = 500, delayMillis = 250) ), exit = slideOutVertically( targetOffsetY = { it }, animationSpec = tween(500) ) ) { Box( modifier = modifier .clip(shape) .background(color = backgroundColor) ){ content() } } } }
CustomSheet:
@Composable fun CustomSheet( viewModel: ChangelogViewModel = hiltViewModel(), navController: NavController ) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination // display only in homeScreen val isHomeScreen = currentDestination?.hierarchy?.any { it.route in listOf( NavigationBarEnum.HOME.route.name(), ) } == true val uiState by viewModel.uiState.collectAsStateWithLifecycle() var delayedVisible by remember { mutableStateOf(false) } LaunchedEffect(uiState.changelog, uiState.visible, isHomeScreen) { val showChangelog = uiState.changelog != null && uiState.visible && isHomeScreen if (showChangelog) { delay(ANIMATION_LONG_DELAY_DURATION.toLong()) delayedVisible = true } else { delayedVisible = false } } uiState.changelog?.let { changelogItem -> CustomSheetWrapper( visible = delayedVisible, modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.85f), ) { ChangelogContent( currentVersion = changelogItem.version, changelog = changelogItem.changelog, onSubmit = { viewModel.onChangelogDismissed() }) } } }
I connect it directly in MainActivity:
NavHost(navController = navController) CustomSheet(navController = navController)
However, underneath is the HomeScreen with a button at the top (the button is completely obscured by the CustomSheet):
@Composable fun ClickableCard(data: Data?, onClick: () -> Unit) { val painter = if (diocese != null) painterResource(diocese.image) else painterResource(R.drawable.example) Card( shape = RoundedCornerShape(Dimens.roundedCornersLarge), elevation = CardDefaults.elevatedCardElevation(Dimens.roundedCornersLarge), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) ) {} }
In Jetpack Compose, a Box with only a background() modifier does not consume pointer events. background paints pixels but stays "transparent" to the input system, so taps fall through to whatever is drawn beneath the Box. Stacking inside a parent is decided by declaration order. Later siblings draw on top, but draw order does not automatically intercept touches.
Add either
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { /* dismiss */ },
)
or
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) { awaitPointerEvent() }
}
}
to the scrim Box, and ideally to the sheets content Box too, so touches are absorbed instead of falling through to the screen below.
Full example:
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onScrimClick, // or {} to just absorb
),
)
You can read more on it in docs: https://developer.android.com/develop/ui/compose/touch-input/pointer-input/understand-gestures?utm_source=chatgpt.com#event-dispatching
> Pointer events are dispatched to a composable hierarchy. The moment that a new pointer triggers its first pointer event, the system starts hit-testing the "eligible" composables. A composable is considered eligible when it has pointer input handling capabilities.
marcinj