Animate visibility in compose
Asked Answered
D

3

12

I have a text which need to be animated to show and hide with the value is null or not. it would have been straight forward if the visibility is separately handle, but this is what I got. In the bellow code the enter animation works but the exit animation dont as the text value is null. I can think of something with remembering the old value but not sure how.

@Composable
fun ShowAnimatedText(
    text : String?
) {
    Column(
        modifier = Modifier.fillMaxWidth()
    ) {
        AnimatedVisibility(
            visible = text != null,
            enter = fadeIn(animationSpec = tween(2000)),
            exit = fadeOut(animationSpec = tween(2000))
        ) {
            text?.let {
                Text(text = it)
            }
        }
    }
}
Dowson answered 17/11, 2022 at 14:16 Comment(2)
What does it mean "but the exit animation dont as the text value is null"?Florenceflorencia
It does not animate, it just disappearedDowson
R
7

I think the fade-out animation is actually working "per-se".

I suspect the parameter text: String? is a value coming from a hoisted "state" somewhere up above ShowAnimatedText, and since you are directly observing it inside the animating scope, when you change it to null it instantly removes the Text composable, and your'e not witnessing a slow fade out.

AnimatedVisibility(
    ...
) {
    text?.let { // your'e directly observing a state over here
        Text(text = it)
    }
}

This is my attempt completing your snippet based on my assumption and making it work, the fade-in works, but the desired fade-out is instantly happening.

@Composable
fun SomeScreen() {

    var text by remember {
        mutableStateOf<String?>("Initial Value")
    }

    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Button(onClick = {
            text = "New Value"
        }) {
            Text("Set New Value")
        }

        Button(onClick = {
            text = null
        }) {
            Text("Remove Value")
        }

        AnimatedText(text = text)
    }
}

@Composable
fun ShowAnimatedText(
    text : String?
) {
    Column(
        modifier = Modifier.fillMaxWidth()
    ) {
        AnimatedVisibility(
            visible = text != null,
            enter = fadeIn(animationSpec = tween(2000)),
            exit = fadeOut(animationSpec = tween(2000))
        ) {
            text?.let {
                Text(text = it)
            }
        }
    }
}

You can solve it by modifying the text to a non-state value and change your visibility logic from using a nullability check to some "business logic" that would require it to be visible or hidden, modifying the codes above like this.

@Composable
fun SomeScreen() {

    var show by remember {
        mutableStateOf(true)
    }

    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Button(onClick = {
            show = !show
        }) {
            Text("Set New Value")
        }

        AnimatedText(text = "Just A Value", show)
    }
}

@Composable
fun ShowAnimatedText(
    text : String?,
    show: Boolean
) {

    Column(
        modifier = Modifier.fillMaxWidth()
    ) {
        AnimatedVisibility(
            visible = show,
            enter = fadeIn(animationSpec = tween(2000)),
            exit = fadeOut(animationSpec = tween(2000))
        ) {

            text?.let {
                Text(text = it)
            }
        }
    }
}

enter image description here

Rabbinism answered 17/11, 2022 at 15:30 Comment(2)
Thank you for the answer. What if we try to remember the old text in the ShowAnimatedText function itself other than sending a placeholder value from the button click? Because for its not only one text that my screen is animating, its a bigger composable with a rather big object as parameter.Dowson
Ohh, then by simply doing this val localText by remember {mutableStateOf(text)} might solve your problemRabbinism
D
1

I fixed it by remembering the previous state (Or don't set the null value) until the exit animation is finished, if the text is null.

Thank you z.y for your suggestion.

@Composable
fun ShowAnimatedText(
    text : String?,
    show: Boolean
) {
    var localText by remember {
        mutableStateOf<String?>(null)
    }
    AnimatedContent(show, localText)
    LaunchedEffect(key1 = text, block = {
        if(text == null){
            delay(2000)
        }
        localText = text
    })
}
@Composable
private fun AnimatedContent(show: Boolean, localText: String?) {
    Column(
        modifier = Modifier.fillMaxWidth()
    ) {
        AnimatedVisibility(
            visible = show,
            enter = fadeIn(animationSpec = tween(2000)),
            exit = fadeOut(animationSpec = tween(2000))
        ) {
            localText?.let {
                Text(text = it)
            }
        }
    }
}
Dowson answered 17/11, 2022 at 16:24 Comment(0)
A
1

Jetpack Compose's AnimatedContent API might be what you want, since you get the data object passed into the lambda and can check there for nullability, e.g.:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ShowAnimatedText(
    text : String?
) {
    Column(
        modifier = Modifier.fillMaxWidth()
    ) {
        AnimatedContent(
            targetState = text,
            transitionSpec = {
                fadeIn(animationSpec = tween(2000)) with
                fadeOut(animationSpec = tween(2000))
            }
        ) { text ->
            text?.let {
                Text(text = it)
            }
        }
    }
}

  • no any animation glitches on hiding (e.g. when the value becomes null)
  • it's also taken into account by the parent layout (e.g. sibling views animate smoothly to make room).

I had the same issue, this answer helped me: https://mcmap.net/q/1011089/-how-to-use-animatedvisibility-with-nullable-values

Atrioventricular answered 16/6, 2023 at 16:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.