Added the ability to set dynamic properties
diff --git a/CHANGELOG_COMPOSE.md b/CHANGELOG_COMPOSE.md
index 2e6fe0d..0cbd03d 100644
--- a/CHANGELOG_COMPOSE.md
+++ b/CHANGELOG_COMPOSE.md
@@ -16,6 +16,8 @@
There are overloaded version of `LottieAnimation` that merge the properties for convenience. Please
refer to the docs for `LottieAnimation`, `LottieAnimatable`, `animateLottieCompositionAsState`
and `rememberLottieComposition` for more information.
+* Added the ability to clip the progress bounds of an animation.
+* Added the ability to set and control dynamic properties.
# 1.0.0-beta07-1
* Compatible with Jetpack Compose Beta 07
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
index 4a8a6c9..5ea2c46 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
@@ -70,6 +70,8 @@
* features so it defaults to off. The only way to know if your animation will work
* well with merge paths or not is to try it. If your animation has merge paths and
* doesn't render correctly, please file an issue.
+ * @param dynamicProperties Allows you to change the properties of an animation dynamically. To use them, use
+ * [rememberLottieDynamicProperties]. Refer to its docs for more info.
*/
@Composable
fun LottieAnimation(
@@ -81,9 +83,11 @@
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
+ dynamicProperties: LottieDynamicProperties? = null,
) {
val drawable = remember { LottieDrawable() }
var imageAssetManager by remember { mutableStateOf<ImageAssetManager?>(null) }
+ var setDynamicProperties: LottieDynamicProperties? by remember { mutableStateOf(null) }
if (composition == null || composition.duration == 0f) return Box(modifier)
@@ -105,6 +109,11 @@
scale(size.width / composition.bounds.width().toFloat(), size.height / composition.bounds.height().toFloat(), Offset.Zero)
}) {
drawable.composition = composition
+ if (dynamicProperties !== setDynamicProperties) {
+ setDynamicProperties?.removeFrom(drawable)
+ dynamicProperties?.addTo(drawable)
+ setDynamicProperties = dynamicProperties
+ }
drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
@@ -137,6 +146,7 @@
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
+ dynamicProperties: LottieDynamicProperties? = null,
) {
val progress by animateLottieCompositionAsState(
composition,
@@ -155,6 +165,7 @@
outlineMasksAndMattes,
applyOpacityToLayers,
enableMergePaths,
+ dynamicProperties,
)
}
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
new file mode 100644
index 0000000..188e375
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
@@ -0,0 +1,167 @@
+package com.airbnb.lottie.compose
+
+import android.graphics.ColorFilter
+import android.graphics.PointF
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import com.airbnb.lottie.LottieDrawable
+import com.airbnb.lottie.model.KeyPath
+import com.airbnb.lottie.value.LottieFrameInfo
+import com.airbnb.lottie.value.LottieValueCallback
+import com.airbnb.lottie.value.ScaleXY
+
+/**
+ * Use this function when you want to apply one or more dynamic properties to an animation.
+ * This takes a vararg of individual dynamic properties which should be created with [rememberLottieDynamicProperty].
+ *
+ * @see rememberLottieDynamicProperty
+ */
+@Composable
+fun rememberLottieDynamicProperties(
+ vararg properties: LottieDynamicProperty<*>,
+): LottieDynamicProperties {
+ @Suppress("UNCHECKED_CAST")
+ return remember(properties) {
+ val intProperties = properties.filter { it.property is Int } as List<LottieDynamicProperty<Int>>
+ val pointFProperties = properties.filter { it.property is PointF } as List<LottieDynamicProperty<PointF>>
+ val floatProperties = properties.filter { it.property is Float } as List<LottieDynamicProperty<Float>>
+ val scaleProperties = properties.filter { it.property is ScaleXY } as List<LottieDynamicProperty<ScaleXY>>
+ val colorFilterProperties = properties.filter { it.property is ColorFilter } as List<LottieDynamicProperty<ColorFilter>>
+ val intArrayProperties = properties.filter { it.property is IntArray } as List<LottieDynamicProperty<IntArray>>
+ LottieDynamicProperties(
+ intProperties,
+ pointFProperties,
+ floatProperties,
+ scaleProperties,
+ colorFilterProperties,
+ intArrayProperties,
+ )
+ }
+}
+
+/**
+ * Use this to create a single dynamic property for an animation.
+ *
+ * @param property should be one of [com.airbnb.lottie.LottieProperty].
+ * @param value the desired value to use as this property's value.
+ * @param keyPath the string parts of a [com.airbnb.lottie.model.KeyPath] that specify which animation element
+ * the property resides on.
+ */
+@Composable
+fun <T> rememberLottieDynamicProperty(
+ property: T,
+ value: T,
+ vararg keyPath: String,
+): LottieDynamicProperty<T> {
+ val keyPathObj = remember(keyPath) { KeyPath(*keyPath) }
+ val callback: (LottieFrameInfo<T>) -> T = remember(value) { { value } }
+ val currentCallback by rememberUpdatedState(callback)
+ return remember(keyPathObj, property) {
+ LottieDynamicProperty(
+ keyPathObj,
+ property,
+ object : LottieValueCallback<T>() {
+ override fun getValue(frameInfo: LottieFrameInfo<T>): T {
+ return currentCallback(frameInfo)
+ }
+ },
+ )
+ }
+}
+
+/**
+ * Use this to create a single dynamic property for an animation.
+ *
+ * @param property Should be one of [com.airbnb.lottie.LottieProperty].
+ * @param keyPath The string parts of a [com.airbnb.lottie.model.KeyPath] that specify which animation element
+ * the property resides on.
+ * @param callback A callback that will be invoked during the drawing pass with current frame info. The frame
+ * info can be used to alter the property's value based on the original animation data or it
+ * can be completely ignored and an arbitrary value can be returned. In this case, you may want
+ * the overloaded version of this function that takes a static value instead of a callback.
+ */
+@Composable
+fun <T> rememberLottieDynamicProperty(
+ property: T,
+ vararg keyPath: String,
+ callback: (frameInfo: LottieFrameInfo<T>) -> T
+): LottieDynamicProperty<T> {
+ val keyPathObj = remember(keyPath) { KeyPath(*keyPath) }
+ val currentCallback by rememberUpdatedState(callback)
+ return remember(keyPathObj, property) {
+ LottieDynamicProperty(
+ keyPathObj,
+ property,
+ object : LottieValueCallback<T>() {
+ override fun getValue(frameInfo: LottieFrameInfo<T>): T {
+ return currentCallback(frameInfo)
+ }
+ },
+ )
+ }
+}
+
+/**
+ * @see rememberLottieDynamicProperty
+ */
+class LottieDynamicProperty<T> internal constructor(
+ internal val keyPath: KeyPath,
+ internal val property: T,
+ internal val valueCallback: LottieValueCallback<T>,
+)
+
+/**
+ * @see rememberLottieDynamicProperties
+ */
+class LottieDynamicProperties internal constructor(
+ private val intProperties: List<LottieDynamicProperty<Int>>,
+ private val pointFProperties: List<LottieDynamicProperty<PointF>>,
+ private val floatProperties: List<LottieDynamicProperty<Float>>,
+ private val scaleProperties: List<LottieDynamicProperty<ScaleXY>>,
+ private val colorFilterProperties: List<LottieDynamicProperty<ColorFilter>>,
+ private val intArrayProperties: List<LottieDynamicProperty<IntArray>>,
+) {
+ internal fun addTo(drawable: LottieDrawable) {
+ intProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.valueCallback)
+ }
+ pointFProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.valueCallback)
+ }
+ floatProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.valueCallback)
+ }
+ scaleProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.valueCallback)
+ }
+ colorFilterProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.valueCallback)
+ }
+ intArrayProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, p.valueCallback)
+ }
+ }
+
+ internal fun removeFrom(drawable: LottieDrawable) {
+ intProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Int>?)
+ }
+ pointFProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<PointF>?)
+ }
+ floatProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Float>?)
+ }
+ scaleProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<ScaleXY>?)
+ }
+ colorFilterProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<ColorFilter>?)
+ }
+ intArrayProperties.forEach { p ->
+ drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<IntArray>?)
+ }
+ }
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 4ef2f0f..a0e26e1 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -1026,7 +1026,7 @@
* {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't.
*/
public <T> void addValueCallback(
- final KeyPath keyPath, final T property, final LottieValueCallback<T> callback) {
+ final KeyPath keyPath, final T property, @Nullable final LottieValueCallback<T> callback) {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
index a810e90..3bd72ec 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/ComposeActivity.kt
@@ -26,6 +26,7 @@
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.sample.compose.examples.AnimatableExamplesPage
import com.airbnb.lottie.sample.compose.examples.BasicUsageExamplesPage
+import com.airbnb.lottie.sample.compose.examples.DynamicPropertiesExamplesPage
import com.airbnb.lottie.sample.compose.examples.ExamplesPage
import com.airbnb.lottie.sample.compose.examples.NetworkExamplesPage
import com.airbnb.lottie.sample.compose.examples.TransitionsExamplesPage
@@ -96,6 +97,7 @@
composable(Route.TransitionsExamples.route) { TransitionsExamplesPage() }
composable(Route.ViewPagerExample.route) { ViewPagerExamplePage() }
composable(Route.NetworkExamples.route) { NetworkExamplesPage() }
+ composable(Route.DynamicProperties.route) { DynamicPropertiesExamplesPage() }
composable(
Route.Player.fullRoute,
arguments = Route.Player.args
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
index daa5f19..2dd9b5d 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/Route.kt
@@ -26,6 +26,8 @@
object NetworkExamples : Route("network examples")
+ object DynamicProperties : Route("dynamic properties examples")
+
object Player : Route(
"player",
listOf(
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/DynamicPropertiesExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/DynamicPropertiesExamplesPage.kt
new file mode 100644
index 0000000..fe33fc4
--- /dev/null
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/DynamicPropertiesExamplesPage.kt
@@ -0,0 +1,155 @@
+package com.airbnb.lottie.sample.compose.examples
+
+import android.graphics.PointF
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.compose.rememberLottieDynamicProperties
+import com.airbnb.lottie.compose.rememberLottieDynamicProperty
+import com.airbnb.lottie.sample.compose.R
+
+@Composable
+fun DynamicPropertiesExamplesPage() {
+ UsageExamplePageScaffold {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ ) {
+ ExampleCard("Heart Color", "Click to change color") {
+ HeartColor()
+ }
+ ExampleCard("Jump Height", "Click to jump heiht") {
+ JumpHeight()
+ }
+ ExampleCard("Change Properties", "Click to toggle whether the dynamic property is used") {
+ ToggleProperty()
+ }
+ }
+ }
+}
+
+@Composable
+private fun HeartColor() {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+ val colors = remember {
+ listOf(
+ Color.Red,
+ Color.Green,
+ Color.Blue,
+ Color.Yellow,
+ )
+ }
+ var colorIndex by remember { mutableStateOf(0) }
+ val color by derivedStateOf { colors[colorIndex] }
+
+ val dynamicProperties = rememberLottieDynamicProperties(
+ rememberLottieDynamicProperty(
+ property = LottieProperty.COLOR,
+ value = color.toArgb(),
+ keyPath = arrayOf(
+ "H2",
+ "Shape 1",
+ "Fill 1",
+ )
+ ),
+ )
+ LottieAnimation(
+ composition,
+ iterations = LottieConstants.IterateForever,
+ dynamicProperties = dynamicProperties,
+ modifier = Modifier
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = { colorIndex = (colorIndex + 1) % colors.size },
+ )
+ )
+}
+
+@Composable
+private fun JumpHeight() {
+ val composition by rememberLottieComposition(LottieCompositionSpec.Asset("AndroidWave.json"))
+ val extraJumpHeights = remember { listOf(0.dp, 24.dp, 48.dp, 128.dp) }
+ var extraJumpIndex by remember { mutableStateOf(0) }
+ val extraJumpHeight by derivedStateOf { extraJumpHeights[extraJumpIndex] }
+ val extraJumpHeightPx = with(LocalDensity.current) { extraJumpHeight.toPx() }
+
+ val point = remember { PointF() }
+ val dynamicProperties = rememberLottieDynamicProperties(
+ rememberLottieDynamicProperty(LottieProperty.TRANSFORM_POSITION, "Body") { frameInfo ->
+ var startY = frameInfo.startValue.y
+ var endY = frameInfo.endValue.y
+ when {
+ startY > endY -> startY += extraJumpHeightPx
+ else -> endY += extraJumpHeightPx
+ }
+ point.set(frameInfo.startValue.x, lerp(startY, endY, frameInfo.interpolatedKeyframeProgress))
+ point
+ }
+ )
+ LottieAnimation(
+ composition,
+ iterations = LottieConstants.IterateForever,
+ dynamicProperties = dynamicProperties,
+ modifier = Modifier
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = { extraJumpIndex = (extraJumpIndex + 1) % extraJumpHeights.size },
+ )
+ )
+}
+
+@Composable
+private fun ToggleProperty() {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
+ var useDynamicProperty by remember { mutableStateOf(true) }
+ val dynamicProperties = rememberLottieDynamicProperties(
+ rememberLottieDynamicProperty(
+ property = LottieProperty.COLOR,
+ value = Color.Green.toArgb(),
+ keyPath = arrayOf(
+ "H2",
+ "Shape 1",
+ "Fill 1",
+ )
+ ),
+ )
+ LottieAnimation(
+ composition,
+ iterations = LottieConstants.IterateForever,
+ dynamicProperties = dynamicProperties.takeIf { useDynamicProperty },
+ modifier = Modifier
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = { useDynamicProperty = !useDynamicProperty },
+ )
+ )
+}
+
+private fun lerp(valueA: Float, valueB: Float, progress: Float): Float {
+ val smallerY = minOf(valueA, valueB)
+ val largerY = maxOf(valueA, valueB)
+ return smallerY + progress * (largerY - smallerY)
+}
\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
index c8eaaa0..e2097ec 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ExamplesPage.kt
@@ -52,5 +52,11 @@
modifier = Modifier
.clickable { navController.navigate(Route.NetworkExamples) }
)
+ ListItem(
+ text = { Text("Dynamic Properties") },
+ secondaryText = { Text("Setting dynamic properties") },
+ modifier = Modifier
+ .clickable { navController.navigate(Route.DynamicProperties) }
+ )
}
}
\ No newline at end of file