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:
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?
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
.