I'm an animation noob so please excuse me if this ...
# compose
c
I'm an animation noob so please excuse me if this comes off as trivial. I have a little tutorial screen I'm making and there is a rubber duck in the center rubber duck and my requirements are to make it "bob" up and down slightly, on an infinite loop. I would have typically maybe considered this using MotionLayout, but is there an easy way to animate this kind of bobbing effect in compose?
t
yeah, check out InfiniteAnimationDemo & InfiniteTransitionDemo if you haven’t already that entire folder has a bunch of great examples
c
Thanks @Tash! I've gotten to about this point and it will probably work for what I need. The speed of the animation seems to slow down when it hits the top, and I think I should convert this to dp as I think my values are in px right now? but it's a start! If anyone else has any suggestions. Please send em my way!
Copy code
@Composable
fun InfiniteAnimationDemo() {
    val alpha = remember { mutableStateOf(0f) }
    LaunchedEffect(Unit) {
        animate(
            initialValue = 20f,
            targetValue = -20f,
            animationSpec =
                infiniteRepeatable(animation = tween(1000), repeatMode = RepeatMode.Reverse)) {
            value,
            _ ->
            alpha.value = value
        }
    }
    Box(Modifier.size(200.dp)) {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = null,
            modifier =
                Modifier.align(Alignment.Center)
                    .graphicsLayer(translationY = alpha.value),
            tint = Color.Red)
    }
}
πŸ‘€ 1
t
have you tried animating the Y offset instead? one of the examples uses
rememberInfiniteTransition()
and a
Dp.VectorConverter
you can use an animation like
keyframes
to get more control:
Copy code
@Composable
fun InfiniteTransitionDemo() {
    val infiniteTransition = rememberInfiniteTransition()

    val offsetY: Dp by infiniteTransition.animateValue(
        initialValue = 0.dp,
        targetValue = 100.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 500
                100.dp.unaryMinus() at 200 with FastOutLinearInEasing // etc
                80.dp at 300 with FastOutSlowInEasing // etc
            }
        )
    )


    Box(Modifier.fillMaxSize()) {
        Icon(
            Icons.Filled.Favorite,
            null,
            Modifier
                .align(Alignment.Center)
                .offset(y = offsetY),
        )
    }
}
πŸ‘ 1
d
Indeed
keyframes
with a few nicely crafted entries would allow the duck to bob more lively... πŸ˜„ The slow-down that you see @Colton Idle is due to the default easing. Here's a good overview of how different easing curves affect the overall movement: https://medium.com/mobile-app-development-publication/android-jetpack-compose-animation-spec-made-easy-6e7990aef203
βž• 1
c
@Tash
have you tried animating the Y offset instead?
I thought that's what I was doing here:
.graphicsLayer(translationY =...
Guess not. πŸ˜„ @Doris Liu oh! Easing. I thought it was due to a default interpolator or something. I tried guessing some interpolator apis but nothing autofilled so I gave up. Easing makes sense though. I have to admit that the code snippet Tash posted is going over my head This piece makes for a fairly frantic animation.
Copy code
animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 500
                100.dp.unaryMinus() at 200 with FastOutLinearInEasing // etc
                80.dp at 300 with FastOutSlowInEasing // etc
            }
        )
@Doris Liu would you recommend the first snippet I posted with a different easing set, or would you reccomend trying to learn the keyframes api and moving forward with that?
d
FastOutLinearInEasing
might work better. But I would recommend playing with
keyframes
. I'm imagining a smaller bounce after the initial bounce. So it'd be alternating between different value ranges. Something like this: πŸ˜„
Copy code
val translationY: Float by infiniteTransition.animateFloat(
        initialValue = -20f,
        targetValue = -20f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 2000
                20f at 600 with LinearOutSlowInEasing
                -10f at 1100
                10f at 1500 with FastOutSlowInEasing // etc
            }
        )
    )
You could also rotate the duck a little as it bobs with another animation in the same infinite transition. πŸ˜›
πŸŽ‰ 1
c
Interesting. I think maybe the example of the bobbing duck wasn't that great as my adesigner basically just made a thing that bobs directly up and down. Nothing crazier than that. It's a slight bob... just enough to make the screen not feel completely static. So I'm really just looking to go up and down on repeat. With that said Doris, would you go for infiniteTransition or infiniteRepeatable?
d
infiniteRepeatable
is the
animationSpec
that will be needed in both cases. But between
InfiniteTransition
and coroutine-based
animate
, it's more of the personal preference when it comes to infinite animations. There's no significant performance difference between the two.
InfiniteTransition
is a convenient API that internally creates a coroutine and run animations in it, which is not that different than what the first snippet is doing. πŸ™‚ With that said, I'd use
InfiniteTransition
in this case since designer might decide to add more animations in the future.
πŸ‘ 1
c
Thanks. That does make a lot of sense to me. For right now this is what I ended up with
Copy code
@Composable
fun InfiniteTransitionDemo() {
    val infiniteTransition = rememberInfiniteTransition()
    val offsetY: Dp by infiniteTransition.animateValue(
        initialValue = 0.dp,
        targetValue = 20.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec =
            infiniteRepeatable(
                animation =
                    keyframes {
                        val duration = 2000
                        durationMillis = duration
                        0.dp at 0 with FastOutSlowInEasing
                        20.dp at duration / 2 with FastOutSlowInEasing
                        0.dp at duration with FastOutSlowInEasing
                    }))
    Box(Modifier.fillMaxSize()) {
        Icon(
            Icons.Filled.Favorite,
            null,
            Modifier.align(Alignment.Center).offset(y = offsetY),
        )
    }
}
I still have two questions if anyone is intrigued: 1. I'm still a bit confused with the significance of the dp values. I want the duck to bob starting at 0, and go up 20dp and then -20dp. So 0 is it's starting point.
Copy code
initialValue = 0.dp,
targetValue = 20.dp,
and
Copy code
0.dp at 0 with FastOutSlowInEasing
20.dp at duration / 2 with FastOutSlowInEasing
0.dp at duration with FastOutSlowInEasing
2. Still not happy with the easing/animation/interpolation. I want it to be springy but the animation not to speed up in the middle. I guess almost like I just want a constant speed but a little bounce.
d
Since the value range is small, there'll be a lot of rounding to the closest integer if you use Dp. For such small range, I'd stick with translation for less rounding and therefore more smoothness. Seems like some tooling around configuring different easing would be helpful. (cc @Chris Sinco [G] ) In the meantime, you could try different curves here: https://www.cssportal.com/css-cubic-bezier-generator/ and create a cubic-bezier easing with the config you like.
πŸ‘πŸΌ 1
c
For 1: Oh. Good idea with the rounding. Yeah I just need the slightest little bob up and down. But... Doris, how do I do a translation πŸ˜… For 2: Oh thanks. That site will be helpful.
d
You had the translation in your first snippet: πŸ˜›
Copy code
...
modifier =
                Modifier.align(Alignment.Center)
                    .graphicsLayer { translationY = /*animated translation value goes here*/ },
πŸ˜€ 1
c
Ah. I'm going insane. lol. animation is tricky and subtle!!!
πŸ˜… 1
Alright. One last question if anyone can answer... I currently have
Copy code
0.dp at 0 with CubicBezierEasing(0.68F, -0.55F, 0.265F, 1.55F)
20.dp at duration / 2 with CubicBezierEasing(0.68F, -0.55F, 0.265F, 1.55F)
0.dp at duration with CubicBezierEasing(0.68F, -0.55F, 0.265F, 1.55F)
because I want to start at 0, go to 20, then back down to 0. That's 3 keyframes but actually two transitions/events, so why do I have to define 3 easings? I think that's whats screwing me over. Shouldn't I just defined two Easings. 1 for each actual event.
t
i dont think you need the first one, so just these two:
Copy code
20.dp at duration / 2 with CubicBezierEasing(0.68F, -0.55F, 0.265F, 1.55F)
0.dp at duration with CubicBezierEasing(0.68F, -0.55F, 0.265F, 1.55F)
does that change anything?
c
It changes it. But still not quite there. The animation when it comes back up and overshoots a bit is perfect, but the speed and downwards motion still needs work
πŸ‘€ 1
I feel like in the view system all I'd be looking for is modifying y axis linearly with an overshoot interpolator. lol
d
with
defines the easing curve for the interval starting at the timestamp you provided, til the next keyframe entry. This means
.. at duration with ..
isn't necessary. BTW, it's very straightforward to impl
Easing
with an OvershootInterpolator πŸ˜„
βž• 1
c
Wait. Setting easing allows you to set an interpolation?
d
Easing and interpolation are the same concept under different names: They take in a fraction, and return a fraction. If the returned value is the same as the input, it is a LinearEasing/LinearInterpolation. πŸ™‚
c
Thanks Doris. I am getting closer! The output of this is still weird even though the code looks pretty air tight to me. Any other tips? initial and target value look good. Duration is good. Repeat mode is set to be in reverse (which seems as what I'd want) and I have an overshoot interpolator. Don't know what else could be wrong.
Copy code
@Composable
fun InfiniteTransitionDemo() {
    val infiniteTransition = rememberInfiniteTransition()
    val offsetY: Dp by infiniteTransition.animateValue(
        initialValue = 0.dp,
        targetValue = 20.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec =
            infiniteRepeatable(
                repeatMode = RepeatMode.Reverse,
                animation =
                    tween(
                        durationMillis = 2000,
                        easing = { OvershootInterpolator().getInterpolation(it) })))
    Box(Modifier.fillMaxSize()) {
        Icon(
            Icons.Filled.Favorite,
            null,
            Modifier.align(Alignment.Center).offset(y = offsetY),
        )
    }
}
d
The code looks good. πŸ™‚ You could try the
BounceInterpolator
also, see the orange curve here: http://dev.antoine-merle.com/blog/2013/04/12/making-a-bounce-animation-for-your-sliding-menu/ It might also help to add a small (~20ms) delay to the tween, so that after it animates back to the beginning it pauses a little.
c
It just ended up looking like a heart on a pogo stick lol
Copy code
@Composable
fun InfiniteTransitionDemo() {
    val infiniteTransition = rememberInfiniteTransition()
    val offsetY: Dp by infiniteTransition.animateValue(
        initialValue = 0.dp,
        targetValue = 20.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec =
            infiniteRepeatable(
                repeatMode = RepeatMode.Reverse,
                animation =
                    tween(
                        delayMillis = 20,
                        durationMillis = 2000,
                        easing = { BounceInterpolator().getInterpolation(it) })))
    Box(Modifier.fillMaxSize()) {
        Icon(
            Icons.Filled.Favorite,
            null,
            Modifier.align(Alignment.Center).offset(y = offsetY),
        )
    }
}
Maybe I just need an example of what I want it to look like, because this looks far off from what my design team wants and I'm failing horribly at making it work. lol
Alright, last attempt until I just quit at it for now πŸ™ƒ
Copy code
@Composable
fun InfiniteTransitionDemo() {
    val infiniteTransition = rememberInfiniteTransition()
    val offsetY: Dp by infiniteTransition.animateValue(
        initialValue = 0.dp,
        targetValue = 20.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec =
            infiniteRepeatable(
                repeatMode = RepeatMode.Reverse,
                animation =
                    tween(
                        durationMillis = 2000,
                        easing = { LinearInterpolator().getInterpolation(it) })))
    Box(Modifier.fillMaxSize()) {
        Icon(
            Icons.Filled.Favorite,
            null,
            Modifier.align(Alignment.Center).offset(y = offsetY),
        )
    }
}
Here is something that works almost exactly like I want it except that when it reaches the high point and the low point I want it to overshoot a little bit/act springy. But I want to keep the linear ease during the duration. I just want it to feel more natural and less robotic. Anyone have any last suggestions?
d
Have you tried asking your designer to give you a handcrafted easing curve? πŸ˜›
c
You have obviously never worked with developers at my company. πŸ™ƒ Needless to say. No. They don't provide me with anything like that. They just say "make this go up and down, but just a little, but make it not so robotic"
πŸ˜‚ 1
The only "example" they gave me is this lottie file. And they said "make it levitate like the person here, but a little less robotic" hence me trying a slight overshoot interpolator. lol https://lottiefiles.com/9626-levitate-meditate-peace-and-love
πŸ˜‚ 3
Where's the joy+sob emoji when you need one. ☠️
c
We are certainly looking at adding more visual tools for playing around with easing curves. Though to Doris’ point, it would be ideal if the designer had something precise in mind. πŸ˜…as a designer, I would have given you something precise πŸ˜…
c
Let me DM you a job application @Chris Sinco [G] πŸ˜‚
πŸ˜† 1
c
Perhaps we need a way to quickly change interpolaters in the animation inspection tools in Preview to aid with iterating on animations live.
c
Yeah. For me it's not even that. It just seems like the easing is being applied like twice. Once it gets to the end of the animation it does what I want it to do, and then it just does the interpolator again seemingly for every interpolator and hand rolled cubic bezier. Almost like a bug, but it's so simple that I think I'm just doing something wrong/I'm just misunderstanding how easing works.
c
I see. One small thing that we’re releasing soon is actually visualizing the easing curves in the animation inspection tool. That way you can at least see when/how the values change over time. Very helpful if not intimately familiar with curves.
c
FWIW I just kinda created it in motion layout. πŸ˜„
Copy code
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="<http://schemas.android.com/apk/res/android>"
    xmlns:motion="<http://schemas.android.com/apk/res-auto>">
    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="2000"
        motion:motionInterpolator="linear">
        <KeyFrameSet>
            <KeyTimeCycle
                android:translationY="5dp"
                motion:framePosition="1"
                motion:motionTarget="@+id/textView"
                motion:waveOffset="0.1"
                motion:wavePeriod="0.5"
                motion:waveShape="cos" />
        </KeyFrameSet>
    </Transition>
</MotionScene>
Maybe the trick is that cos wave shape
It has that wavy/levitating/bobbing feel to it. Not really a linear start and stop sort of thing. Which is interesting because the interpolator is "linear"
d
There's actually an interpolator for sine waves too πŸ˜„: https://developer.android.com/reference/android/view/animation/CycleInterpolator
c
Oh boy. Need to pull out the high school text books for this one. πŸ˜…
Alright. So I definitely don't understand how it works
Copy code
easing = { CycleInterpolator(3f).getInterpolation(it) })))
this is soooo close. It just jumps "randomly" (probably not randomly, its probably doing exactly what I told it to)
Oh fuck yeah! Replacing RepeatMode.Reverse with Restart did the trick. Now that I think about it... maybe this was the issue all along that was causing all of those weird bounces?
Copy code
infiniteRepeatable(
        repeatMode = RepeatMode.Restart,
        animation =
        tween(
                durationMillis = 5000,
                easing = { CycleInterpolator(3f).getInterpolation(it) })))
Thanks Doris and Chris for steering me in the right direction. I still gotta admit that I don't understand why the other techniques didn't work. Oh 🐳
πŸŽ‰ 2
d
The effect of Restart vs. Reverse on value change should be much easier to visualize once you can see the curves for
repeatables
in the animation inspector tool that Chris mentioned. πŸ˜„
πŸ•΅οΈβ€β™‚οΈ 1
πŸ‘ 1
s
Hey, I was reading this thread, but I am realizing I’ve never gotten the animation inspector tool to work like that for me. Is there some part of the documentation that talks about it and how to work with it? Trying on Canary 7 atm btw
d
@Stylianos Gakis do you not see the curves or no inspector at all?
s
Hey, you answered my question here actually. Thanks for helping out btw! And I am super looking forward to even more APIs being supported by the tool πŸ₯³
❀️ 1