Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack compose - how to use a value animation to directly control other animations

I have a case in which it would be useful to use a single animated float value to control the animations of other elements, including fading between two colors. Is there a suggested way of doing this in general, like declaring an Animatable whose current state value is directly controlled by an external mutable float state? For example, if the mutable float is 0.25 at a particular instance, then all animations that it controls would be 25% of the way between one state and another state.

The reason I want this behavior is to force multiple animations to be perfectly in sync, even when leaving and reentering the composition. I know that transitions are commonly used to control multiple animations, but it is my understanding that this does not ensure all child animations are perfectly synced, i.e. at the exact same percent of the way completed.

It should be possible to accomplish this by brute force by having a single animatable float value, and using that value to directly set the position or colors of UI elements. Is this the best solution? If I use this method, I still need to calculate the interpolation between two colors, and I'm not entirely sure how to do this. I tried digging into the Compose source code to find how this is done by the animateColorAsState() composable. It seems that the colors are converted into 4D vectors, and I imagine they are linearly interpolated from there but I couldn't find the exact code that does this. Is there a built in function to interpolate between colors or vectors? Otherwise, I could just compute the value myself, but I want to try to find a cleaner way to implement all of this.

Any thoughts are appreciated!

like image 372
Corey Cleasby Avatar asked Dec 11 '25 00:12

Corey Cleasby


1 Answers

Jetpack compose has linear interpolation function defined, lerp, for various classes including Rect, Color, FontSize, Size, Offset, Shadow and many classes except for Float, Int and Long.

For the last three you need to add

implementation "androidx.compose.ui:ui-util:$compose_version"

or you can copy paste them as

/**
 * Linearly interpolate between [start] and [stop] with [fraction] fraction between them.
 */
fun lerp(start: Float, stop: Float, fraction: Float): Float {
    return (1 - fraction) * start + fraction * stop
}

/**
 * Linearly interpolate between [start] and [stop] with [fraction] fraction between them.
 */
fun lerp(start: Int, stop: Int, fraction: Float): Int {
    return start + ((stop - start) * fraction.toDouble()).roundToInt()
}

/**
 * Linearly interpolate between [start] and [stop] with [fraction] fraction between them.
 */
fun lerp(start: Long, stop: Long, fraction: Float): Long {
    return start + ((stop - start) * fraction.toDouble()).roundToLong()
}

In addition to linear interpolation sometimes scaling function which lets you change range from 0f, 1f to any range you want can be defined as

// Scale x1 from a1..b1 range to a2..b2 range
private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
    androidx.compose.ui.util.lerp(a2, b2, calcFraction(a1, b1, x1))


// Calculate the 0..1 fraction that `pos` value represents between `a` and `b`
private fun calcFraction(a: Float, b: Float, pos: Float) =
    (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)

Using these 2 functions and with one Animatable or any animateFloatAsState you can synchronise many aniamtions with one value.

In this example below, lerp and scale is used for changing position of Rect, text size, color and offset of a card.

Linear Interpolation Animation

@Composable
fun SnackCard(
    modifier: Modifier = Modifier,
    snack: Snack,
    progress: Float = 0f,
    textColor: Color,
    onClick: () -> Unit
) {

    Box(
        modifier = modifier
            // 🔥 Interpolate corner radius
            .clip(RoundedCornerShape(lerp(20.dp, 0.dp, progress)))
            .background(Color.White)
            .clickable(
                onClick = onClick,
                interactionSource = remember { MutableInteractionSource() },
                indication = null
            ),
        contentAlignment = Alignment.TopEnd
    ) {


        // 🔥 This is lerping between .6f and 1f by changing start from 0f to .6f
        val fraction = scale(0f, 1f, progress, .6f, 1f)

        Image(
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxWidth()
                .fillMaxHeight(fraction),
            painter = rememberAsyncImagePainter(
                ImageRequest.Builder(LocalContext.current).data(data = snack.imageUrl)
                    .apply(block = fun ImageRequest.Builder.() {
                        crossfade(true)
                        placeholder(drawableResId = R.drawable.placeholder)
                    }).build()
            ),
            contentDescription = null
        )

        Column(
            modifier = Modifier
                .padding(16.dp)
                .align(Alignment.BottomStart)

        ) {
            Text(
                // 🔥 Interpolate Font size
                fontSize = lerp(18.sp, 40.sp, progress),
                // 🔥 Interpolate Color
                color = lerp(textColor, Color.Black, progress),
                fontWeight = FontWeight.Bold,
                text = snack.name
            )
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text(
                    // 🔥 Interpolate Font size
                    fontSize = lerp(12.sp, 24.sp, progress),
                    // 🔥 Interpolate Color
                    color = lerp(textColor, Color.Black, progress),
                    text = "$${snack.price}"
                )
            }
        }

        FavoriteButton(
            modifier = Modifier.graphicsLayer {
                alpha = 1 - progress
            }
                .padding(12.dp),
            color = textColor
        )
    }
}

Full code is available here

https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials/blob/master/Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter6_graphics/Tutorial6_30LinearInterpolation.kt

like image 158
Thracian Avatar answered Dec 13 '25 13:12

Thracian



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!