Home > database >  Jetpack Compose animation finished listener not called
Jetpack Compose animation finished listener not called

Time:11-09

I'm struggling to use Jetpack Compose Animation to achieve a (supposedly simple) effect: in the case of an error, a control's background color should flash red, and after a short delay then fade back to normal (transparent).

My current approach is to model this with a boolean state shouldFlash which is set to true when an error occurs, and is set back to false when the animation completes. Unfortunately it seems the finishedListener passed to animateColorAsState is never called. I attached a debugger and also added a log statement to verify this.

What am I doing wrong?

Sample (button triggers error):

@Composable
fun FlashingBackground() {
    Column(modifier = Modifier.size(200.dp)) {
        var shouldFlash by remember { mutableStateOf(false) }
        var text by remember { mutableStateOf("Initial") }
        FlashingText(flashing = shouldFlash, text = text) {
            shouldFlash = false
            text = "Animation done"
        }

        Button(onClick = {
            shouldFlash = true
        }) {
            Text(text = "Flash")
        }
    }
}

@Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
    if (flashing) {
        val flashColor by animateColorAsState(
            targetValue = Color.Red,
            finishedListener = { _ -> flashFinished() }
        )
        Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
    } else {
        Text(text = text)
    }
}

Compose version: 1.0.5 (latest stable at time of writing), also in 1.1.0-beta02

CodePudding user response:

Anyway, to understand this, you need to know how the animateColorAsState works internally.

It relies on recompositions - is the core idea.

Every time a color changes, a recomposition is triggered, which results in the updated color value being reflected on the screen. Now, what you are doing is just using conditional statements to DISPLAY DIFFERENT COMPOSABLES. Now, one Composable is actually referring to the animating value, that is, the one inside your if block (when flashing is true). On the other hand, the else block Composable is just a regular text which does not reference it. That is why you need to remove the conditional. Anyway, because after removing the conditional, what remains is only a single text, I thought it would be a waste to create a whole new Composable out of it, which is why I removed that Composable altogether and pasted the Text inside your main Composable. It helps to keep things simpler enough. Other than this, the answer by @Rafiul does work, but there is not really a need for a Composable like that, so I would still recommend using this answer instead, so that the code is easier to read.

ORIGINAL ANSWER:

Try moving the animator outside the Child Composable

@Composable
fun FlashingBackground() {
    Column(modifier = Modifier.size(200.dp)) {
        var shouldFlash by remember { mutableStateOf(false) }
        var text by remember { mutableStateOf("Initial") }
        val flashFinished: (Color) -> Unit = {
            shouldFlash = false
            text = "Animation done"
        }
        val flashColor by animateColorAsState(
            targetValue = if (shouldFlash) Color.Red else Color.White,
            finishedListener = flashFinished
        )

        //FlashingText(flashing = shouldFlash, text = text) -> You don't need this
        Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
        Button(onClick = {
            shouldFlash = true
        }) {
            Text(text = "Flash")
        }
    }
}

CodePudding user response:

Change your code like this.

FlashingBackground

@Composable
fun FlashingBackground() {
    Column(modifier = Modifier.size(200.dp)) {
        var shouldFlash by remember { mutableStateOf(false) }
        var text by remember { mutableStateOf("Initial") }
        FlashingText(flashing = shouldFlash, text = text) {
            shouldFlash = false
            text = "Animation done"
        }

        Button(onClick = {
            shouldFlash = true
        }) {
            Text(text = "Flash")
        }
    }
}

FlashingText

@Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
        val flashColor by animateColorAsState(
            targetValue = if(flashing) Color.Red else Color.White,
            finishedListener = { _ -> flashFinished() }
        )
        Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
}

Edited:

The problem with your code is you are initializing animateColorAsState when you are clicking the Flash button and making shouldFlash = true. So for the first time, it just initializes the animateColorAsState, doesn't run the animation. So there will be no finishedListener call as well. Since finishedListener isn't executed, shouldFlash stays to true. So from the next call shouldFlash is already true there will be no state change. That's why from the subsequent button click, it doesn't recompose the FlashingText anymore. You can put some log in your method you won't see FlashingText after the first button click.

Keep in mind: targetValue = Color.Red will not do anything. target value should be either a state or like this condition if(flashing) Color.Red because you need to change the state to start the animation.

@Phillip's answer is also right. But I don't see any extra advantage in moving the animator outside the Child Composable if you use it like the above.

  • Related