Skip to content

Commit

Permalink
[Compose] Added the ability to set dynamic properties (#1831)
Browse files Browse the repository at this point in the history
Example usage can be seen on DynamicPropertiesExamplesPage.kt.

Thanks to Compose's impressive snapshot system, states that are read during the drawing pass via the dynamic properties callback are automatically registered so invalidation happens correctly by default.
  • Loading branch information
gpeal authored Jul 4, 2021
1 parent dd5ff8f commit 7150c66
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG_COMPOSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and setting an image asset delegate are on the `LottieAnimation` composable.
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import com.airbnb.lottie.setImageAssetManager
* 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(
Expand All @@ -81,9 +83,11 @@ fun LottieAnimation(
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)

Expand All @@ -105,6 +109,11 @@ fun LottieAnimation(
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)
Expand Down Expand Up @@ -137,6 +146,7 @@ fun LottieAnimation(
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
) {
val progress by animateLottieCompositionAsState(
composition,
Expand All @@ -155,6 +165,7 @@ fun LottieAnimation(
outlineMasksAndMattes,
applyOpacityToLayers,
enableMergePaths,
dynamicProperties,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 {
return remember(properties) {
LottieDynamicProperties(properties.toList())
}
}

/**
* 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) }
return remember(keyPathObj, property, value) {
LottieDynamicProperty(property, keyPathObj, value)
}
}

/**
* 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 callbackState by rememberUpdatedState(callback)
return remember(keyPathObj, property) {
LottieDynamicProperty(
property,
keyPathObj,
) { callbackState(it) }
}
}

/**
* @see rememberLottieDynamicProperty
*/
class LottieDynamicProperty<T> internal constructor(
internal val property: T,
internal val keyPath: KeyPath,
internal val callback: (frameInfo: LottieFrameInfo<T>) -> T,
) {
constructor(property: T, keyPath: KeyPath, value: T) : this(property, keyPath, { value })
}

/**
* @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>>,
) {
@Suppress("UNCHECKED_CAST")
constructor(properties: List<LottieDynamicProperty<*>>) : this(
properties.filter { it.property is Int } as List<LottieDynamicProperty<Int>>,
properties.filter { it.property is PointF } as List<LottieDynamicProperty<PointF>>,
properties.filter { it.property is Float } as List<LottieDynamicProperty<Float>>,
properties.filter { it.property is ScaleXY } as List<LottieDynamicProperty<ScaleXY>>,
properties.filter { it.property is ColorFilter } as List<LottieDynamicProperty<ColorFilter>>,
properties.filter { it.property is IntArray } as List<LottieDynamicProperty<IntArray>>,
)

internal fun addTo(drawable: LottieDrawable) {
intProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
pointFProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
floatProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
scaleProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
colorFilterProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
intArrayProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
}

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>?)
}
}
}

private fun <T> ((frameInfo: LottieFrameInfo<T>) -> T).toValueCallback() = object : LottieValueCallback<T>() {
override fun getValue(frameInfo: LottieFrameInfo<T>): T {
return invoke(frameInfo)
}
}
2 changes: 1 addition & 1 deletion lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ public List<KeyPath> resolveKeyPath(KeyPath keyPath) {
* {@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.navigation.compose.rememberNavController
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
Expand Down Expand Up @@ -96,6 +97,7 @@ class ComposeActivity : AppCompatActivity() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ sealed class Route(val route: String, val args: List<NamedNavArgument> = emptyLi

object NetworkExamples : Route("network examples")

object DynamicProperties : Route("dynamic properties examples")

object Player : Route(
"player",
listOf(
Expand Down
Loading

0 comments on commit 7150c66

Please sign in to comment.