I'm looking at the animation API and either I'm mi...
# compose
z
I'm looking at the animation API and either I'm missing something (most likely) or it's harder to implement this using Compose. I want to implement a shake/wiggle animation. Imagine a
TextField
and a button. When the user clicks the button, if the value in the
TextField
is not correct or empty, I'd like to move the
TextField
to the left/right a few times to draw the user's attention. In a
View
world I can do something like this:
Copy code
ObjectAnimator
  .ofFloat(myTextInput, "translationX", 0, 25, -25, 25, -25,15, -15, 6, -6, 0)
  .setDuration(duration)
  .start();
But with Compose I can't figure out a way to have the current and target value be the same. The direction I went into is to use
animateDpAsState
together with
keyframes
animation spec to animate the
offset
of my
TextField
and it does the job. My issue is how to trigger the animation.
1
This is what I've been playing with, but
targetValue
is 1dp in the error case (so that the animation can start). And then when the animation completes and I reset
invalidInput
trigger, the animation plays again (that's another thing that I have to figure out how to tackle).
Copy code
var invalidInput by remember { mutableStateOf(false) }

    val offset: Dp by animateDpAsState(
        targetValue = if (invalidInput) 1.dp else 0.dp,
        animationSpec = keyframes {
            durationMillis = 500
            16.dp at 30
            (-16).dp at 60
            16.dp at 90
            (-16).dp at 140
            8.dp at 200
            (-8).dp at 260
            4.dp at 330
            (-4).dp at 400
        },
        finishedListener = {
            invalidInput = false
        }
    )
z
The lower level animation apis are probably better for this
z
Yep, I read that page a few times already 😄 But there we also have
animateTo
that I assume would require a value different from the current one. Something else that I could do is to chain 2 animations one after the other where the first one has target value set to one of the keyframes. The second animation could then animate back to 0 offset. But there has to be a simpler, more elegant solution.
z
the thing about assumptions… 😉 This works great:
Copy code
val scope = rememberCoroutineScope()
val offset = remember { Animatable(0f) }

Box(Modifier.fillMaxSize()) {
  Button(
    onClick = {
      scope.launch {
        offset.animateTo(0f, animationSpec = keyframes {
          durationMillis = 500
          16f at 30
          (-16f) at 60
          16f at 90
          (-16f) at 140
          8f at 200
          (-8f) at 260
          4f at 330
          (-4f) at 400
        })
      }
    },
    modifier = Modifier.offset { IntOffset(x = offset.value.dp.roundToPx(), y = 0) }
  ) {
    Text("shake me")
  }
}
cheers 1
✔️ 2
K 2
🙏 2
yes black 1
👍 4
🦜 1
K 1
Although it’s probably better to animate the view with a
graphicsLayer
since that doesn’t involve triggering another layout
1
Copy code
Modifier.graphicsLayer { translationX = offset.value.dp.toPx() }
z
Oh 🤯 that's working nice! Thanks a lot! I shouldn't have assumed that behaviour about animating to the current value. 🙏 Re:
graphicsLayer
- I was thinking the same. I just wanted to play around and see if/how to accomplish the desired effect before polishing and optimizing it.
👍🏻 1