Compose AnimatedVisibility escapes container

5.4k Views Asked by At

I am using Jetpack Compose (version 1.1.1) and I am trying to show an snackbar-style alert coming from the top of the screen using AnimatedVisibility. However, when I do so the alert escapes its container and overlaps the content above it (in this case, the action bar). Also, the content below it (the button) waits until the animation is finished before adjusting its position. This results in a jumpy effect. I would like it to slide down in sync with the alert.

result

@Composable
fun HomeScreen() {
    val showAlert = remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()
    Column(Modifier.fillMaxHeight()) {
        ActionBar("Home Screen")
        Column {
            Alert(visible = showAlert.value)
            Button(modifier = Modifier.padding(16.dp), onClick = {
                scope.launch {
                    showAlert.value = true
                    delay(1000)
                    showAlert.value = false
                }
            }) {
                Text("Show Alert")
            }
        }

    }

}

@Composable
private fun ActionBar(title: String) {
    TopAppBar(backgroundColor = Color.Blue, contentColor = Color.White) {
        Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp, end = 8.dp)) {
            Text(title)
        }
    }
}

@Composable
fun Alert(visible: Boolean) {
    AnimatedVisibility(visible = visible, enter = slideInVertically(), exit = slideOutVertically()) {
        Column(Modifier.fillMaxWidth().background(Color.Red)) {
            Text("An error has occurred", Modifier.padding(16.dp), style = TextStyle(Color.White))
        }
    }

}

@Preview
@Composable
fun HomeScreenPreview() {
    Box(Modifier.fillMaxSize().background(Color.White)) {
        HomeScreen()
    }
}
1

There are 1 best solutions below

2
On BEST ANSWER

For the overlap issue, you can use the modifier: Modifier.clipToBounds()

For the 'jumpy effect' you can put the alert text and the page content in a single column and animate the offset.

enum class AlertState {
    Collapsed,
    Expanded
}

@Preview
@Composable
fun HomeScreen() {
    val scope = rememberCoroutineScope()
    val alertHeight = 40.dp
    var currentState by remember { mutableStateOf(AlertState.Collapsed) }
    val transition = updateTransition(currentState, label = "")
    val alertOffset by transition.animateDp(label = "") {
        when (it) {
            AlertState.Collapsed -> -alertHeight
            AlertState.Expanded -> 0.dp
        }
    }
    Column(Modifier.fillMaxHeight()) {
        TopAppBar(backgroundColor = Color.Blue, contentColor = Color.White) {
            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.padding(start = 8.dp, end = 8.dp)
            ) {
                Text("Home Screen")
            }
        }
        Column(
            modifier = Modifier.clipToBounds().offset(y = alertOffset)
        ) {
            Row(
                Modifier.height(alertHeight).fillMaxWidth().background(Color.Red).padding(start = 16.dp)
            ) {
                Text(
                    "An error has occurred", style = TextStyle(Color.White),
                    modifier = Modifier.align(Alignment.CenterVertically)
                )
            }
            Button(modifier = Modifier.padding(16.dp), onClick = {
                scope.launch {
                    currentState = AlertState.Expanded
                    delay(1000)
                    currentState = AlertState.Collapsed
                }
            }) {
                Text("Show Alert")
            }
        }
    }
}

snap

If your alert height is not fix, you can get it using the modifier Modifier.onGloballyPositioned