Corey Cleasby
Corey Cleasby

Reputation: 95

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!

Upvotes: 3

Views: 3206

Answers (3)

Thracian
Thracian

Reputation: 67248

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

Upvotes: 10

Jan Bína
Jan Bína

Reputation: 7278

You pretty much answered yourself, I think that having one animated float from 0 to 1 and use that to interpolate between two colors or anything else is the best solution in your case. And compose actually provides methods to calculate those interpolations, they are all called lerp - for example this one for colors or this one for Dps.

Upvotes: 2

Cook915
Cook915

Reputation: 68

I think transitions are what you are looking for, as they can be used to animate many properties together in sync. Here is a code snippet from the official docs.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

As well as a link to the docs this from where it was pulled.

Upvotes: 0

Related Questions