https://kotlinlang.org logo
Title
j

jean

04/21/2022, 10:38 AM
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

Zach Klippenstein (he/him) [MOD]

04/21/2022, 5:08 PM
Is your animation spec using Reverse mode? The default is Repeat
j

jean

04/26/2022, 1:14 PM
yes I was using the reverse value
this is my 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

Zach Klippenstein (he/him) [MOD]

04/26/2022, 6:21 PM
Hm, I’m not sure if you can do that with this API. I might drop down to
Animatable
, e.g. something like:
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

jean

04/26/2022, 6:23 PM
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 :
@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

Zach Klippenstein (he/him) [MOD]

04/26/2022, 6:28 PM
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
.graphicsLayer {
  scaleX = value
  scaleY = value
}
j

jean

04/26/2022, 6:54 PM
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
@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
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

Zach Klippenstein (he/him) [MOD]

04/27/2022, 3:15 PM
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

jean

04/27/2022, 3:51 PM
thanks for the advices 🙂