Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 24, 2021 05:14 pm GMT

Compose Animations beyond the state change

Jetpack Compose hit 1.0 a few weeks ago, and it came with a wonderful and robust animations API. With this new API, you will have total control over animations when the state changes. But when it comes to more complex scenarios, you may face some non-obvious paths. This article will explore some of these scenarios and help you understand how you can achieve your goal.

I'm about to discuss the problems I found when trying to implement the AVLoadingIndicatorView library in Compose. And to guide you through this journey, I'll use the following loading indicators as examples.

Kapture 2021-08-18 at 18.02.33

Also, all the code we discuss in this article is available in this repository:

BallScaleIndicator

Let's start with some simple animation. The animation consists in reducing alpha while increasing the scale from a circle. We can do that with just one value that will move from 0 to 1. So the scale will be the current value, and the alpha will be the complementary value (1 - currentValue). As this is a loading animation, we will have to configure it to repeat forever.

TL;DR; we would have to do something like this:

@Composablefun BallScaleIndicator() {    val animationProgress by animateFloatAsState(        targetValue = 1f,        animationSpec = infiniteRepeatable(            animation = tween(durationMillis = 800)        )    )    Ball(        modifier = Modifier            .scale(animationProgress)            .alpha(1 - animationProgress),    )}

But when you run your app, this is the result:

Loading indicator not working

Nothing happens. Why? In the first lines from this article, we said that Compose has an awesome API to animate state changes, but this is not some state change. We would have to add one variable to control the target state and change it to start the animation. Also, when the view is rendered, we would have to change the value to start the animation.

@Composablefun BallScaleIndicator() {    // Create one target value state that will     // change to start the animation    var targetValue by remember { mutableStateOf(0f) }    // Update the attribute on the animation    val animationProgress by animateFloatAsState(        targetValue = targetValue,        animationSpec = infiniteRepeatable(            animation = tween(durationMillis = 800)        )    )    // Use the SideEffect helper to run something    // when this block runs    SideEffect { targetValue = 1f }    Ball(        modifier = Modifier            .scale(animationProgress)            .alpha(1 - animationProgress),    )}

And now, everything works

Loading indicator with side effect

But we have another problem now. The SideEffect will run every time the view is recomposed. So as the animation changes the state value on each iteration, we will run this effect many times. It makes the animation work, but it may not scale nicely and cause some UI issues.

Compose also provides a way to animate values using transitions. One type of transition is the InfiniteTransition. It provides you a syntax with the initial and final values as parameters and it's automatically started when created (no need for SideEffects). To use it, you need to create an instance of it using the rememberInfiniteTransition method and call the animateFloat function to have an animated state.

After a small refactor, this is the result.

@Composablefun BallScaleIndicator() {    // Creates the infinite transition    val infiniteTransition = rememberInfiniteTransition()    // Animate from 0f to 1f    val animationProgress by infiniteTransition.animateFloat(        initialValue = 0f,        targetValue = 1f,        animationSpec = infiniteRepeatable(            animation = tween(durationMillis = 800)        )    )    Ball(        modifier = Modifier            .scale(animationProgress)            .alpha(1 - animationProgress),    )}

And the result animation is the same, but without using side effects on this function.

Loading indicator with infinite transition

BallPulseSyncIndicator

Alright, we got the first one. Let's try something more complex. This one consists of three balls jumping in synchrony. The easiest way to achieve this is to delay the start of each animation. We can have an animation time of 600ms and start it with a delay of 70ms for each ball.

On a quick search into the compose animation API, we find that the tween animation has a property delayMillis that we can use to implement this behavior. And to animate the values, we can keep the InfiniteTransition. So let's start working with it.

@Composablefun BallPulseSyncIndicator() {    val infiniteTransition = rememberInfiniteTransition()    val animationValues = (1..3).map { index ->        infiniteTransition.animateFloat(            initialValue = 0f,            targetValue = 12f,            animationSpec = infiniteRepeatable(                animation = tween(                    durationMillis = 300,                    delayMillis = 70 * index,                ),                repeatMode = RepeatMode.Reverse,            )        )    }    Row {        animationValues.forEach { animatedValue ->            Ball(                modifier = Modifier                    .padding(horizontal = 4.dp)                    .offset(y = animatedValue.value.dp),            )        }    }}

Sound good, right? But when we see the animation, we will notice something weird.

Loading indicator losing synchrony

You can see that, after some running time, the animation loses synchrony and starts behaving weirdly. The reason for that is the property that we used to delay the animation. It applies the delay to each iteration, not just the first one.

The solution to this is to use the Coroutines Animation API. It's provided by the Compose animations and has a method called animate. It has a syntax pretty similar to the animateFloat from transition. With that in mind, we can use the delay function from Coroutines before starting the animation. This will guarantee the correct behavior.

@Composablefun BallPulseSyncIndicator() {    val animationValues = (1..3).map { index ->        var animatedValue by remember { mutableStateOf(0f) }        LaunchedEffect(key1 = Unit) {            // Delaying using Coroutines            delay(70L * index)            animate(                initialValue = 0f,                targetValue = 12f,                animationSpec = infiniteRepeatable(                    // Remove delay property                    animation = tween(durationMillis = 300),                    repeatMode = RepeatMode.Reverse,                )            ) { value, _ -> animatedValue = value }        }        animatedValue    }    Row {        animationValues.forEach { animatedValue ->            Ball(                modifier = Modifier                    .padding(horizontal = 4.dp)                    .offset(y = animatedValue.dp),            )        }    }}

Now, the animation will keep synchronized, even after some time.

Loading indicator with delay to start

TriangleSkewSpinIndicator

Alright, let's go to the next one. This triangle indicator has two animations (rotation on the X-axis and Y-axis), but you need to wait for the previous one to execute to start the next one. So if we put that in a timeline, we will have something like this:

Untitled

The easiest way to evaluate something like this is to handle the animation as one thing with groups. Each group will be formed by the value and the next item in the list and take the same amount to evaluate.

Untitled (1)

Rewriting the image in Kotlin, we would have something like this:

@Composablefun animateValues(    values: List<Float>,    animationSpec: AnimationSpec<Float> = spring(),): State<Float> {    // 1. Create the groups zipping with next entry    val groups by rememberUpdatedState(newValue = values.zipWithNext())    // 2. Start the state with the first value    val state = remember { mutableStateOf(values.first()) }    LaunchedEffect(key1 = groups) {        val (_, setValue) = state        // Start the animation from 0 to groups quantity        animate(            initialValue = 0f,            targetValue = groups.size.toFloat(),            animationSpec = animationSpec,        ) { frame, _ ->            // Get which group is being evaluated            val integerPart = frame.toInt()            val (initialValue, finalValue) = groups[frame.toInt()]            // Get the current "position" from the group animation            val decimalPart = frame - integerPart            // Calculate the progress between the initial and final value            setValue(                initialValue + (finalValue - initialValue) * decimalPart            )        }    }    return state}

With this one implemented, the animation process will be pretty simple. Just create two variables to hold the X and Y rotation, and update the view using the .graphicsLayer modifier.

@Composablefun TriangleSkewSpinIndicator() {    val animationSpec = infiniteRepeatable<Float>(        animation = tween(            durationMillis = 2500,            easing = LinearEasing,        )    )    val xRotation by animateValues(        values = listOf(0f, 180f, 180f, 0f, 0f),         animationSpec = animationSpec    )    val yRotation by animateValues(        values = listOf(0f, 0f, 180f, 180f, 0f),         animationSpec = animationSpec    )    Triangle(        modifier = Modifier.graphicsLayer(            rotationX = xRotation,            rotationY = yRotation,        )    )}

And this is the result:

Triangle indicator example

Final Thoughts

The Jetpack Compose comes with an incredible animation API. It provides you many ways to implement all kinds of animations you may need. But, the current API is a bit different from the imperative version, and some paths will not be that obvious.

To help you have a smooth transition to compose, Touchlab has started a project to help you with all these non-obvious paths.

Compose Animations

Group of libraries to help you build better animations with Jetpack Compose

About

Goal

The main goal of this project is to help you build animations with Jetpack Compose. The composeanimations API provides a rich animation API to handle state changes, but you need to implementsome boilerplate code when it comes to other types of animation.

What's Included?

  1. Easings - A library with 30 different easings to use on your animations
  2. Value Animator Compat - Compatibility API with non-compose animationsusing ValueAnimator
  3. Value Animator - API that provides you the same functionality fromnon-compose ValueAnimator, but using only compose methods

About Touchlab

Touchlab is a mobile-focused development agency based in NYC. We have been working on Android sincethe beginning, and have worked on a wide range of mobile and hardware projects for the past decadeOver the past few years, we have invested significantly on

Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @faogustavo on Twitter, the Kotlin Slack, or AndroidDevBr Slack. And if you find all this interesting, maybe you'd like to work with or work at Touchlab.


Original Link: https://dev.to/touchlab/compose-animations-beyond-the-state-change-234a

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To