Add dynamic properties for images
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
index ef791a9..c6333a1 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt
@@ -1,5 +1,6 @@
 package com.airbnb.lottie.compose
 
+import android.graphics.Bitmap
 import android.graphics.ColorFilter
 import android.graphics.PointF
 import android.graphics.Typeface
@@ -97,6 +98,7 @@
     private val colorFilterProperties: List<LottieDynamicProperty<ColorFilter>>,
     private val intArrayProperties: List<LottieDynamicProperty<IntArray>>,
     private val typefaceProperties: List<LottieDynamicProperty<Typeface>>,
+    private val bitmapProperties: List<LottieDynamicProperty<Bitmap>>,
 ) {
     @Suppress("UNCHECKED_CAST")
     constructor(properties: List<LottieDynamicProperty<*>>) : this(
@@ -107,6 +109,7 @@
         properties.filter { it.property is ColorFilter } as List<LottieDynamicProperty<ColorFilter>>,
         properties.filter { it.property is IntArray } as List<LottieDynamicProperty<IntArray>>,
         properties.filter { it.property is Typeface } as List<LottieDynamicProperty<Typeface>>,
+        properties.filter { it.property is Bitmap } as List<LottieDynamicProperty<Bitmap>>,
     )
 
     internal fun addTo(drawable: LottieDrawable) {
@@ -131,6 +134,10 @@
         typefaceProperties.forEach { p ->
             drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
         }
+        bitmapProperties.forEach { p ->
+            drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
+        }
+
     }
 
     internal fun removeFrom(drawable: LottieDrawable) {
@@ -155,6 +162,9 @@
         typefaceProperties.forEach { p ->
             drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Typeface>?)
         }
+        bitmapProperties.forEach { p ->
+            drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Bitmap>?)
+        }
     }
 }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
index faa7e9e..f1acbe6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
@@ -56,7 +56,12 @@
   }
 
   /**
-   * TODO
+   * Permanently sets the bitmap on this LottieImageAsset. This will:
+   * 1) Overwrite any existing Bitmaps.
+   * 2) Apply to *all* animations that use this LottieComposition.
+   *
+   * If you only want to replace the bitmap for this animation, use dynamic properties
+   * with {@link LottieProperty#IMAGE}.
    */
   public void setBitmap(@Nullable Bitmap bitmap) {
     this.bitmap = bitmap;
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java b/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
index 0686156..e28128c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
@@ -1,5 +1,7 @@
 package com.airbnb.lottie;
 
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.ColorFilter;
 import android.graphics.PointF;
 import android.graphics.Typeface;
@@ -163,8 +165,17 @@
   Float TEXT_SIZE = 14f;
 
   ColorFilter COLOR_FILTER = new ColorFilter();
-
+  /**
+   * Array of ARGB colors that map to position stops in the original gradient.
+   * For example, a gradient from red to blue could be remapped with [0xFF00FF00, 0xFFFF00FF] (green to purple).
+   */
   Integer[] GRADIENT_COLOR = new Integer[0];
-
+  /**
+   * Set on text layers.
+   */
   Typeface TYPEFACE = Typeface.DEFAULT;
+  /**
+   * Set on image layers.
+   */
+  Bitmap IMAGE = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
index fa855ae..1653449 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
@@ -25,6 +25,7 @@
   private final Rect src = new Rect();
   private final Rect dst = new Rect();
   @Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
+  @Nullable private BaseKeyframeAnimation<Bitmap, Bitmap> imageAnimation;
 
   ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) {
     super(lottieDrawable, layerModel);
@@ -60,6 +61,11 @@
 
   @Nullable
   private Bitmap getBitmap() {
+    if (imageAnimation != null) {
+      Bitmap callbackBitmap = imageAnimation.getValue();
+      if (callbackBitmap != null)
+        return callbackBitmap;
+    }
     String refId = layerModel.getRefId();
     return lottieDrawable.getImageAsset(refId);
   }
@@ -76,6 +82,14 @@
         colorFilterAnimation =
             new ValueCallbackKeyframeAnimation<>((LottieValueCallback<ColorFilter>) callback);
       }
+    } else if (property == LottieProperty.IMAGE) {
+      if (callback == null) {
+        imageAnimation = null;
+      } else {
+        //noinspection unchecked
+        imageAnimation =
+            new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Bitmap>) callback);
+      }
     }
   }
 }
diff --git a/sample-compose/src/main/assets/Images/android.png b/sample-compose/src/main/assets/Images/android.png
new file mode 100644
index 0000000..4bb66b0
--- /dev/null
+++ b/sample-compose/src/main/assets/Images/android.png
Binary files differ
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ImagesExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ImagesExamplesPage.kt
index b7934bf..0bedae9 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ImagesExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ImagesExamplesPage.kt
@@ -11,13 +11,18 @@
 import androidx.compose.runtime.LaunchedEffect
 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.platform.LocalContext
-import com.airbnb.lottie.LottieImageAsset
+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
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
@@ -36,8 +41,11 @@
             ExampleCard("Assets Image", "Image stored in assets") {
                 ImageAssets()
             }
-            ExampleCard("Assets Image Callback", "Load an image manually") {
-                ImageAssetCallback()
+            ExampleCard("Dynamic Properties", "Replace an image with dynamic properties") {
+                DynamicProperties()
+            }
+            ExampleCard("Store on LottieImageAsset", "Store the bitmap within LottieImageAsset") {
+                StoredOnImageAsset()
             }
         }
     }
@@ -72,17 +80,40 @@
 }
 
 @Composable
-fun ImageAssetCallback() {
+fun DynamicProperties() {
+    // Don't cache the composition so the bitmaps can get released once the animation is no longer being used.
+    val composition by rememberLottieComposition(
+        LottieCompositionSpec.RawRes(R.raw.we_accept),
+        cacheKey = null,
+    )
+    val bitmap = rememberBitmapFromAssets("Images/android.png")
+
+    val dynamicProperties = rememberLottieDynamicProperties(
+        rememberLottieDynamicProperty(LottieProperty.IMAGE, bitmap, "weaccept.jpg")
+    )
+
+    LottieAnimation(
+        composition,
+        iterations = LottieConstants.IterateForever,
+        dynamicProperties = dynamicProperties,
+    )
+}
+
+@Composable
+fun StoredOnImageAsset() {
     // Don't cache the composition so the bitmaps can get released once the animation is no longer being used.
     val composition by rememberLottieComposition(
         LottieCompositionSpec.RawRes(R.raw.we_accept),
         cacheKey = null,
     )
     val imageAsset by derivedStateOf { composition?.images?.get("image_0") }
-    val context = LocalContext.current
-    LaunchedEffect(imageAsset) {
-        withContext(Dispatchers.IO) {
-            imageAsset?.bitmap = loadBitmapFromAssets(context, imageAsset)
+    val bitmap = rememberBitmapFromAssets("Images/android.png")
+    LaunchedEffect(imageAsset, bitmap) {
+        if (imageAsset != null && bitmap != null) {
+            // this stores the bitmap on the original composition's image asset which means that it
+            // will affect *all* LottieAnimation composables that are rendering this LottieComposition.
+            // Use with caution.
+            imageAsset?.bitmap = bitmap
         }
     }
     LottieAnimation(
@@ -91,25 +122,27 @@
     )
 }
 
-private fun loadBitmapFromAssets(context: Context, asset: LottieImageAsset?): Bitmap? {
+@Composable
+private fun rememberBitmapFromAssets(asset: String): Bitmap? {
+    var bitmap: Bitmap? by remember { mutableStateOf(null) }
+    val context = LocalContext.current
+    LaunchedEffect(asset) {
+        withContext(Dispatchers.IO) {
+            bitmap = loadBitmapFromAssets(context, asset)
+        }
+    }
+    return bitmap
+}
+
+private fun loadBitmapFromAssets(context: Context, asset: String?): Bitmap? {
     asset ?: return null
     return try {
-        val inputSteam = context.assets.open("Images/WeAccept/${asset.fileName}")
+        val inputSteam = context.assets.open(asset)
         val opts = BitmapFactory.Options()
         opts.inScaled = true
         opts.inDensity = 1606
-        val bitmap = BitmapFactory.decodeStream(inputSteam, null, opts)
-        bitmap?.resizeTo(asset.width, asset.height)
+        BitmapFactory.decodeStream(inputSteam, null, opts)
     } catch (e: Exception) {
         null
     }
-}
-
-private fun Bitmap.resizeTo(width: Int, height: Int): Bitmap? {
-    if (width == width && height == height) {
-        return this
-    }
-    val resizedBitmap = Bitmap.createScaledBitmap(this, width, height, true)
-    recycle()
-    return resizedBitmap
 }
\ No newline at end of file