Thread
#compose
    Zhelyazko Atanasov

    Zhelyazko Atanasov

    1 year ago
    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:
    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.
    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).
    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
            }
        )
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    The lower level animation apis are probably better for this
    Zhelyazko Atanasov

    Zhelyazko Atanasov

    1 year ago
    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.
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    the thing about assumptions… 😉 This works great:
    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")
      }
    }
    Although it’s probably better to animate the view with a
    graphicsLayer
    since that doesn’t involve triggering another layout
    Modifier.graphicsLayer { translationX = offset.value.dp.toPx() }
    Zhelyazko Atanasov

    Zhelyazko Atanasov

    1 year ago
    Oh 😒hocked_face_with_exploding_head: that's working nice! Thanks a lot! I shouldn't have assumed that behaviour about animating to the current value. :thank-you: 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.