androidandroid-jetpack-composejetpack-compose-animation

LaunchedEffect Animation Plays Repeatedly for Same Composable


I'm making a dice roller app where you can select dice to add them to the "field". When a die is selected, it appears on the screen and a little jiggle animation plays to simulate it being tossed onto a surface. The selected dice are shown in a sorted vertical list. The issue I'm having is that whenever more than 1 die is selected, the last die in the list always does the animation, rather than the die that was just selected:

enter image description here

It seems like when the LazyColumn is getting recomposed to show the newly selected die, it's considering the last die in the list to always be the new one, so it's running its LaunchedEffect block each time. This is how I'm doing the animation on the die:

@Composable
fun DieIcon(
    die: Die,
    animated: Boolean
) {
    val interactionSource = remember { MutableInteractionSource() }
    var currentRotation by remember { mutableFloatStateOf(15f) }
    val rotation = remember { Animatable(currentRotation) }

    LaunchedEffect(Unit) {
        if (animated) {
            val result = rotation.animateTo(
                targetValue = -15f,
                animationSpec = repeatable(
                    iterations = 3,
                    animation = tween(
                        durationMillis = 50,
                        easing = LinearEasing
                    ),
                    repeatMode = RepeatMode.Reverse
                )
            ) {
                currentRotation = rotation.value
            }

            if (result.endReason == AnimationEndReason.Finished) {
                rotation.animateTo(
                    targetValue = 0f,
                    animationSpec = tween(
                        durationMillis = 200,
                        easing = LinearOutSlowInEasing
                    )
                ) {
                    currentRotation = rotation.value
                }
            }
        }
    }

    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .rotate(
                if (animated) {
                    currentRotation
                } else {
                    0f
                }
            )
    ) {
        // show die image here
    }
}

And this is how I'm calling DieIcon:

@Composable
private fun DiceList(
    dice: List<Die>,
    modifier: Modifier = Modifier
) {
    val state = rememberLazyListState()

    LaunchedEffect(dieUIModels.last()) {
        state.animateScrollToItem(dieUIModels.size)
    }

    LazyColumn(
        state = state,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(vertical = 32.dp),
        modifier = modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp)
    ) {
        items(dice) { die->
            Box(
                contentAlignment = Alignment.Center
            ) {
                DieIcon(
                    die = die,
                    animated = true
                )
            }
        }
    }
}

The dice list is sorted in the view model and then collected as state in another composable that calls this one. How can I make the animation run for the newly selected die each time?


Solution

  • As per the items documentation, there's a second parameter you aren't using - key:

    a factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. Type of the key should be saveable via Bundle on Android. If null (the default) is passed, the position in the list will represent the key.

    So because you aren't overriding the key, when you add a die to the list, the LazyColumn doesn't know that you're adding it to the beginning of the list - from its point of view, the only key that was added was the index of the position at the end of the list.

    So what you need is a key that is based on the die object itself - that way, the die at position 0 moves to position 1 when you add a new die at the beginning of the list. That way, the LazyColumn knows that the first element is the new one and correctly runs its LaunchedEffect.