How can I animate a Box to increase its size and r...
# compose
j
How can I animate a Box to increase its size and revert the animation to its original size right after? I did try to use
animateFloatAsState
with 2 iterations but when the animation ends, the size of the box jumps to the
targetValue
z
Is your animation spec using Reverse mode? The default is Repeat
j
yes I was using the reverse value
this is my code :
Copy code
val size by animateDpAsState(
    targetValue = if (selected) 16.dp else 8.dp,
    animationSpec = repeatable(
        iterations = 2,
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse,
    )
)
...
Box(
    modifier = modifier
        .size(size)
        .clip(CircleShape)
        .background(color)
)
the problem here is that the size of the box goes from 8 to 16 and back to 8 but then it jumps to 16 at the end of the animation.
z
Hm, I’m not sure if you can do that with this API. I might drop down to
Animatable
, e.g. something like:
Copy code
val sizeAnimatable = remember { Animatable(…) }
LaunchedEffect(sizeAnimatable) {
  snapshotFlow { selected }.collect { selected ->
    // Launch allows the animation system to handle
    // interruption itself, e.g. to preserve velocity.
    launch {
      if (selected) {
        sizeAnimatable.animateTo(16.dp)
      }
      sizeAnimatable.animateTo(8.dp)
    }
  }
}
j
I just found that
Animatable
coupled with the
scale
attribute. I got inspiration from this thread https://kotlinlang.slack.com/archives/CJLTWPH7S/p1650534011825809 I managed to make it work but it’s far from pretty code, I still need to work on it
I’m using an internal composable function inside my composable function (🤢) because the animation is triggered before the layout is rendered I think :
Copy code
@Composable
fun triggerScaleAnimation(): Float {
    if (selected) {
        LaunchedEffect(secretCircleState) {
            animatedScale.animateTo(targetValue = 1.25f, animationSpec = tween(300))
            animatedScale.animateTo(targetValue = 1f, animationSpec = tween(300))
        }
    }
    return animatedScale.value
}
...
Box(
    modifier = modifier
        .size(8.dp)
        .scale(animatedScale.value)
        .clip(CircleShape)
        .background(color)
)
triggerScaleAnimation()
z
You generally don’t want to read animated values from composition, since it triggers an unnecessary recomposition on every frame when the value is only used for layout or graphics layer properties.
triggerScaleAnimation
should return a
State<Float>
and instead of
.scale(value)
do
Copy code
.graphicsLayer {
  scaleX = value
  scaleY = value
}
j
My first thought was to use
return derivedStateOf { animatedScale.value }
to return a
State<Float>
but that makes the animation not work. How would you create the state object?
Turned out it works with this
Copy code
@Composable
fun bounceAnimation(
    selected: Boolean,
    scaleFactor: Float,
    durationMillis: Int,
): State<Float> {
    val animatedScale = remember { Animatable(1f) }
    if (selected) {
        LaunchedEffect(selected) {
            animatedScale.animateTo(targetValue = scaleFactor, animationSpec = tween(durationMillis / 2))
            animatedScale.animateTo(targetValue = 1f, animationSpec = tween(durationMillis / 2))
        }
    }
    return derivedStateOf { animatedScale.value }
}
and then using it like this
Copy code
val scale by bounceAnimation(
    selected = secretCircleState == SecretCircleState.SELECTED,
    scaleFactor = 1.25f,
    durationMillis = 300,
)

Box(
    modifier = modifier
        .size(8.dp)
        .scale(scale)
        .clip(CircleShape)
        .background(color)
        /*.graphicsLayer { if I use .graphicsLayer instead of .scale, it doesn't work
            scaleX = scale
            scaleY = scale
        }*/
)
z
animatedScale.asState()
You don’t need it here because of
asState()
, but if you’re calling
derivedStateOf
in a composable you need to wrap it in a
remember
j
thanks for the advices 🙂