Compose: LottieAnimation recomposes on every frame degrading performance

diff --git a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
index 81acbedf..aeecf2fb 100755
--- a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
+++ b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
@@ -22,6 +22,6 @@ class ComposeIssueReproActivity : AppCompatActivity() {
     fun Content() {
         val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
         val progress by animateLottieCompositionAsState(composition)
-        LottieAnimation(composition, progress)
+        LottieAnimation(composition, { progress })
     }
 }
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 f22be003..fefea855 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
@@ -32,12 +32,12 @@ import kotlin.math.roundToInt
  *
  * @param composition The composition that will be rendered. To generate a [LottieComposition], you can use
  *                    [rememberLottieComposition].
- * @param progress The progress (between 0 and 1) that should be rendered. If you want to render a specific
- *                 frame, you can use [LottieComposition.getFrameForProgress]. In most cases, you will want
- *                 to use one of the overloaded LottieAnimation composables that drives the animation for you.
- *                 The overloads that have isPlaying as a parameter instead of progress will drive the
- *                 animation automatically. You may want to use this version if you want to drive the animation
- *                 from your own Animatable or via events such as download progress or a gesture.
+ * @param progressProvider A provider for the progress (between 0 and 1) that should be rendered. If you want to render a
+ *                         specific frame, you can use [LottieComposition.getFrameForProgress]. In most cases, you will want
+ *                         to use one of the overloaded LottieAnimation composables that drives the animation for you.
+ *                         The overloads that have isPlaying as a parameter instead of progress will drive the
+ *                         animation automatically. You may want to use this version if you want to drive the animation
+ *                         from your own Animatable or via events such as download progress or a gesture.
  * @param outlineMasksAndMattes Enable this to debug slow animations by outlining masks and mattes.
  *                              The performance overhead of the masks and mattes will be proportional to the
  *                              surface area of all of the masks/mattes combined.
@@ -69,7 +69,7 @@ import kotlin.math.roundToInt
 @Composable
 fun LottieAnimation(
     composition: LottieComposition?,
-    @FloatRange(from = 0.0, to = 1.0) progress: Float,
+    @FloatRange(from = 0.0, to = 1.0) progressProvider: () -> Float,
     modifier: Modifier = Modifier,
     outlineMasksAndMattes: Boolean = false,
     applyOpacityToLayers: Boolean = false,
@@ -114,16 +114,52 @@ fun LottieAnimation(
             drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
             drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
             drawable.clipToCompositionBounds = clipToCompositionBounds
-            drawable.progress = progress
+            drawable.progress = progressProvider()
             drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
             drawable.draw(canvas.nativeCanvas, matrix)
         }
     }
 }

+/**
+ * This is like [LottieAnimation] except that it takes a raw progress parameter instead of taking a progress provider.
+ *
+ * @see LottieAnimation
+ */
+@Composable
+fun LottieAnimation(
+    composition: LottieComposition?,
+    @FloatRange(from = 0.0, to = 1.0) progress: Float,
+    modifier: Modifier = Modifier,
+    outlineMasksAndMattes: Boolean = false,
+    applyOpacityToLayers: Boolean = false,
+    enableMergePaths: Boolean = false,
+    renderMode: RenderMode = RenderMode.AUTOMATIC,
+    maintainOriginalImageBounds: Boolean = false,
+    dynamicProperties: LottieDynamicProperties? = null,
+    alignment: Alignment = Alignment.Center,
+    contentScale: ContentScale = ContentScale.Fit,
+    clipToCompositionBounds: Boolean = true,
+) {
+    LottieAnimation(
+        composition,
+        { progress },
+        modifier,
+        outlineMasksAndMattes,
+        applyOpacityToLayers,
+        enableMergePaths,
+        renderMode,
+        maintainOriginalImageBounds,
+        dynamicProperties,
+        alignment,
+        contentScale,
+        clipToCompositionBounds,
+    )
+}
+
 /**
  * This is like [LottieAnimation] except that it handles driving the animation via [animateLottieCompositionAsState]
- * instead of taking a raw progress parameter.
+ * instead of taking a progress provider.
  *
  * @see LottieAnimation
  * @see animateLottieCompositionAsState
@@ -157,7 +193,7 @@ fun LottieAnimation(
     )
     LottieAnimation(
         composition,
-        progress,
+        { progress },
         modifier,
         outlineMasksAndMattes,
         applyOpacityToLayers,
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
index 562b837a..f52b15dd 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
@@ -64,7 +64,7 @@ private fun Example1() {
             iterations = LottieConstants.IterateForever,
         )
     }
-    LottieAnimation(anim.composition, anim.progress)
+    LottieAnimation(anim.composition, { anim.progress })
 }

 @Composable
@@ -84,7 +84,7 @@ private fun Example2() {
         }
     }
     Box {
-        LottieAnimation(anim.composition, anim.progress)
+        LottieAnimation(anim.composition, { anim.progress })
         Slider(
             value = sliderGestureProgress ?: anim.progress,
             onValueChange = { sliderGestureProgress = it },
@@ -110,7 +110,7 @@ private fun Example3() {
         )
     }
     Box {
-        LottieAnimation(composition, anim.progress)
+        LottieAnimation(composition, { anim.progress })
         Slider(
             value = speed,
             onValueChange = { speed = it },
@@ -144,7 +144,7 @@ private fun Example4() {
     }
     LottieAnimation(
         composition,
-        animatable.progress,
+        { animatable.progress },
         modifier = Modifier
             .clickable { nonce++ }
     )
@@ -162,7 +162,7 @@ private fun Example5() {
     }
     LottieAnimation(
         composition,
-        animatable.progress,
+        { animatable.progress },
         modifier = Modifier
             .clickable { shouldPlay = !shouldPlay }
     )
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
index d592c18f..f82e2ba9 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
@@ -140,7 +140,7 @@ private fun Example6() {
     )
     LottieAnimation(
         composition,
-        progress,
+        { progress },
     )
 }

diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
index e6eec4e3..0dbcd85d 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
@@ -90,7 +90,7 @@ fun SingleCompositionTransition(section: TransitionSection) {
             } while (s == TransitionSection.LoopMiddle)
         }
     }
-    LottieAnimation(composition, animatable.progress)
+    LottieAnimation(composition, { animatable.progress })
 }

 @Composable
@@ -113,5 +113,5 @@ fun SplitCompositionTransition(section: TransitionSection) {
         )
     }

-    LottieAnimation(animatable.composition, animatable.progress)
+    LottieAnimation(animatable.composition, { animatable.progress })
 }
\\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
index 26a51469..79945bef 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
@@ -60,7 +60,7 @@ private fun WalkthroughAnimation(pagerState: PagerState) {
     val progress by derivedStateOf { (pagerState.currentPage + pagerState.currentPageOffset) / (pagerState.pageCount - 1f) }
     LottieAnimation(
         composition,
-        progress,
+        { progress },
         modifier = Modifier
             .fillMaxSize()
     )
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
index e050069d..3a640f27 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
@@ -255,7 +255,7 @@ fun PlayerPageContent(
         ) {
             PlayerPageLottieAnimation(
                 composition,
-                state.animatable.progress,
+                { state.animatable.progress },
                 modifier = Modifier
                     // TODO: figure out how maxWidth can play nice with the aspectRatio modifier inside of LottieAnimation.
                     .fillMaxWidth()
@@ -291,12 +291,12 @@ fun PlayerPageContent(
 @Composable
 private fun PlayerPageLottieAnimation(
     composition: LottieComposition?,
-    progress: Float,
+    progressProvider: () -> Float,
     modifier: Modifier = Modifier,
 ) {
     LottieAnimation(
         composition,
-        progress,
+        progressProvider,
         modifier = modifier,
     )
 }

Compose: LottieAnimation recomposes on every frame degrading performance
Issue Repro Compose Fork : [https://github.com/MSDarwish2000/lottie-android-performance](Fork)

**Describe the bug**
`LottieAnimation` recomposes itself on every frame. This is completely unnecessary in most cases as the only actual change happens during the draw phase, not composition or layout phases. This happens as a result of passing `progress` to `LottieAnimation`. Even when using the function without progress, this happens internally reducing the scope of recomposition but not preventing it.

**Steps To Reproduce**
Steps to reproduce the behavior:

1. This can be reproduced with any Lottie Compose sample using Layout Inspector in Android Studio Dolphin
2. This can be benchmarked using the code in the issue above or any similar code

**Sample benchmark result**

- _While running minified release variant_
 Drawing time: 1174.3ms - Recomposition time: 979.4ms - percentage: 45.48%
- _While running debug variant_
 Drawing time: 1369.3ms - Recomposition time: 1219ms - percentage: 47.1%
- _While debugging_
 Drawing time: 2398.3ms - Recomposition time: 1837.2ms - percentage: 43.38%

_Note:_ The percentage of recomposition time may increase if the parent composable had more code which is very likely

package com.airbnb.lottie.issues

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.issues.databinding.IssueReproActivityBinding

class IssueReproActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = IssueReproActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // Reproduce any issues here.
    }
}

package com.airbnb.lottie.compose

import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.withInfiniteAnimationFrameNanos
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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.runtime.withFrameNanos
import com.airbnb.lottie.LottieComposition
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.job
import kotlinx.coroutines.withContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext

/**
 * Use this to create a [LottieAnimatable] in a composable.
 *
 * @see LottieAnimatable
 */
@Composable
fun rememberLottieAnimatable(): LottieAnimatable = remember { LottieAnimatable() }

/**
 * Use this to create a [LottieAnimatable] outside of a composable such as a hoisted state class.
 *
 * @see rememberLottieAnimatable
 * @see LottieAnimatable
 */
fun LottieAnimatable(): LottieAnimatable = LottieAnimatableImpl()

/**
 * Reset the animation back to the minimum progress and first iteration.
 */
suspend fun LottieAnimatable.resetToBeginning() {
    snapTo(
        progress = defaultProgress(composition, clipSpec, speed),
        iteration = 1,
    )
}

/**
 * [LottieAnimatable] is an extension of [LottieAnimationState] that contains imperative
 * suspend functions to control animation playback.
 *
 * To create one, call:
 * ```
 * val animatable = rememberLottieAnimatable()
 * ```
 *
 * This is the imperative version of [animateLottieCompositionAsState].
 *
 * [LottieAnimationState] ensures *mutual exclusiveness* on its animations. To
 * achieve this, when a new animation is started via [animate] or [snapTo], any ongoing
 * animation will be canceled via a [CancellationException]. Because of this, it is possible
 * that your animation will not start synchronously. As a result, if you switch from animating
 * one composition to another, it is not safe to render the second composition immediately after
 * calling animate. Instead, you should always rely on [LottieAnimationState.composition] and
 * [LottieAnimationState.progress].
 *
 * This class is comparable to [androidx.compose.animation.core.Animatable]. It is a relatively
 * low-level API that gives maximum control over animations. In most cases, you can use
 * [animateLottieCompositionAsState] which provides declarative APIs to create, update, and animate
 * a [LottieComposition].
 *
 * @see animate
 * @see snapTo
 * @see animateLottieCompositionAsState
 */
@Stable
interface LottieAnimatable : LottieAnimationState {
    /**
     * Snap to a specific point in an animation. This can be used to update the progress
     * or iteration count of an ongoing animation. It will cancel any ongoing animations
     * on this state class. To update and then resume an animation, call [animate] again with
     * continueFromPreviousAnimate set to true after calling [snapTo].
     *
     * @param composition The [LottieComposition] that should be rendered.
     *                    Defaults to [LottieAnimatable.composition].
     * @param progress The progress that should be set.
     *                 Defaults to [LottieAnimatable.progress]
     * @param iteration Updates the current iteration count. This can be used to "rewind" or
     *                  "fast-forward" an ongoing animation to a past/future iteration count.
     *                   Defaults to [LottieAnimatable.iteration]
     * @param resetLastFrameNanos [rememberLottieAnimatable] keeps track of the frame time of the most
     *                            recent animation. When [animate] is called with continueFromPreviousAnimate
     *                            set to true, a delta will be calculated from the most recent [animate] call
     *                            to ensure that the original progress is unaffected by [snapTo] calls in the
     *                            middle.
     *                            Defaults to false if progress is not being snapped to.
     *                            Defaults to true if progress is being snapped to.
     */
    suspend fun snapTo(
        composition: LottieComposition? = this.composition,
        progress: Float = this.progress,
        iteration: Int = this.iteration,
        resetLastFrameNanos: Boolean = progress != this.progress,
    )

    /**
     * Animate a [LottieComposition].
     *
     * @param composition The [LottieComposition] that should be rendered.
     * @param iteration The iteration to start the animation at. Defaults to 1 and carries over from previous animates.
     * @param iterations The number of iterations to continue running for. Set to 1 to play one time
     *                   set to [LottieConstants.IterateForever] to iterate forever. Can be set to arbitrary
     *                   numbers. Defaults to 1 and carries over from previous animates.
     * @param speed The speed at which the composition should be animated. Can be negative. Defaults to 1 and
     *              carries over from previous animates.
     * @param clipSpec An optional [LottieClipSpec] to trim the playback of the composition between two values.
     *                 Defaults to null and carries over from previous animates.
     * @param initialProgress An optional progress value that the animation should start at. Defaults to the
     *                        starting progress as defined by the clipSpec and speed. Because the default value
     *                        isn't the existing progress value, if you are resuming an animation, you
     *                        probably want to set this to [progress].
     * @param continueFromPreviousAnimate When set to true, instead of starting at the minimum progress,
     *                                    the initial progress will be advanced in accordance to the amount
     *                                    of time that has passed since the last frame was rendered.
     * @param cancellationBehavior The behavior that this animation should have when cancelled. In most cases,
     *                             you will want it to cancel immediately. However, if you have a state based
     *                             transition and you want an animation to finish playing before moving on to
     *                             the next one then you may want to set this to [LottieCancellationBehavior.OnIterationFinish].
     * @param ignoreSystemAnimationsDisabled When set to true, the animation will animate even if animations are disabled at the OS level.
     *                                       Defaults to false.
     */
    suspend fun animate(
        composition: LottieComposition?,
        iteration: Int = this.iteration,
        iterations: Int = this.iterations,
        speed: Float = this.speed,
        clipSpec: LottieClipSpec? = this.clipSpec,
        initialProgress: Float = defaultProgress(composition, clipSpec, speed),
        continueFromPreviousAnimate: Boolean = false,
        cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediately,
        ignoreSystemAnimationsDisabled: Boolean = false,
    )
}

@Stable
private class LottieAnimatableImpl : LottieAnimatable {
    override var isPlaying: Boolean by mutableStateOf(false)
        private set

    override var progress: Float by mutableStateOf(0f)
        private set

    override val value: Float
        get() = progress

    override var iteration: Int by mutableStateOf(1)
        private set

    override var iterations: Int by mutableStateOf(1)
        private set

    override var clipSpec: LottieClipSpec? by mutableStateOf(null)
        private set

    override var speed: Float by mutableStateOf(1f)
        private set

    override var composition: LottieComposition? by mutableStateOf(null)
        private set

    override var lastFrameNanos: Long by mutableStateOf(AnimationConstants.UnspecifiedTime)
        private set

    private val endProgress: Float by derivedStateOf {
        val c = composition
        when {
            c == null -> 0f
            speed < 0 -> clipSpec?.getMinProgress(c) ?: 0f
            else -> clipSpec?.getMaxProgress(c) ?: 1f
        }
    }

    override val isAtEnd: Boolean by derivedStateOf { iteration == iterations && progress == endProgress }

    private val mutex = MutatorMutex()

    override suspend fun snapTo(
        composition: LottieComposition?,
        progress: Float,
        iteration: Int,
        resetLastFrameNanos: Boolean,
    ) {
        mutex.mutate {
            this.composition = composition
            this.progress = progress
            this.iteration = iteration
            isPlaying = false
            if (resetLastFrameNanos) {
                lastFrameNanos = AnimationConstants.UnspecifiedTime
            }
        }
    }

    override suspend fun animate(
        composition: LottieComposition?,
        iteration: Int,
        iterations: Int,
        speed: Float,
        clipSpec: LottieClipSpec?,
        initialProgress: Float,
        continueFromPreviousAnimate: Boolean,
        cancellationBehavior: LottieCancellationBehavior,
        ignoreSystemAnimationsDisabled: Boolean,
    ) {
        mutex.mutate {
            this.iteration = iteration
            this.iterations = iterations
            this.speed = speed
            this.clipSpec = clipSpec
            this.composition = composition
            this.progress = initialProgress
            if (!continueFromPreviousAnimate) lastFrameNanos = AnimationConstants.UnspecifiedTime
            if (composition == null) {
                isPlaying = false
                return@mutate
            } else if (speed.isInfinite()) {
                progress = endProgress
                isPlaying = false
                this.iteration = iterations
                return@mutate
            }

            isPlaying = true
            try {
                val context = when (cancellationBehavior) {
                    LottieCancellationBehavior.OnIterationFinish -> NonCancellable
                    LottieCancellationBehavior.Immediately -> EmptyCoroutineContext
                }
                val parentJob = coroutineContext.job
                withContext(context) {
                    while (true) {
                        val actualIterations = when (cancellationBehavior) {
                            LottieCancellationBehavior.OnIterationFinish -> {
                                if (parentJob.isActive) iterations else iteration
                            }
                            else -> iterations
                        }
                        if (!doFrame(actualIterations)) break
                    }
                }
                coroutineContext.ensureActive()
            } finally {
                isPlaying = false
            }
        }
    }

    private suspend fun doFrame(iterations: Int): Boolean {
        return if (iterations == LottieConstants.IterateForever) {
            // We use withInfiniteAnimationFrameNanos because it allows tests to add a CoroutineContext
            // element that will cancel infinite transitions instead of preventing composition from ever going idle.
            withInfiniteAnimationFrameNanos { frameNanos ->
                onFrame(iterations, frameNanos)
            }
        } else {
            withFrameNanos { frameNanos ->
                onFrame(iterations, frameNanos)
            }
        }
    }

    private fun onFrame(iterations: Int, frameNanos: Long): Boolean {
        val composition = composition ?: return true
        val dNanos = if (lastFrameNanos == AnimationConstants.UnspecifiedTime) 0L else (frameNanos - lastFrameNanos)
        lastFrameNanos = frameNanos

        val minProgress = clipSpec?.getMinProgress(composition) ?: 0f
        val maxProgress = clipSpec?.getMaxProgress(composition) ?: 1f

        val dProgress = dNanos / 1_000_000 / composition.duration * speed
        val progressPastEndOfIteration = when {
            speed < 0 -> minProgress - (progress + dProgress)
            else -> progress + dProgress - maxProgress
        }
        if (progressPastEndOfIteration < 0f) {
            progress = progress.coerceIn(minProgress, maxProgress) + dProgress
        } else {
            val durationProgress = maxProgress - minProgress
            val dIterations = (progressPastEndOfIteration / durationProgress).toInt() + 1

            if (iteration + dIterations > iterations) {
                progress = endProgress
                iteration = iterations
                return false
            }
            iteration += dIterations
            val progressPastEndRem = progressPastEndOfIteration - (dIterations - 1) * durationProgress
            progress = when {
                speed < 0 -> maxProgress - progressPastEndRem
                else -> minProgress + progressPastEndRem
            }
        }

        return true
    }
}

private fun defaultProgress(composition: LottieComposition?, clipSpec: LottieClipSpec?, speed: Float): Float {
    return when {
        speed < 0 && composition == null -> 1f
        composition == null -> 0f
        speed < 0 -> clipSpec?.getMaxProgress(composition) ?: 1f
        else -> clipSpec?.getMinProgress(composition) ?: 0f
    }
}

package com.airbnb.lottie.compose

import android.graphics.Matrix
import androidx.annotation.FloatRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieDrawable
import com.airbnb.lottie.RenderMode
import com.airbnb.lottie.utils.Utils
import kotlin.math.roundToInt

/**
 * This is the base LottieAnimation composable. It takes a composition and renders it at a specific progress.
 *
 * The overloaded version of [LottieAnimation] that handles playback and is sufficient for most use cases.
 *
 * @param composition The composition that will be rendered. To generate a [LottieComposition], you can use
 *                    [rememberLottieComposition].
 * @param progress The progress (between 0 and 1) that should be rendered. If you want to render a specific
 *                 frame, you can use [LottieComposition.getFrameForProgress]. In most cases, you will want
 *                 to use one of the overloaded LottieAnimation composables that drives the animation for you.
 *                 The overloads that have isPlaying as a parameter instead of progress will drive the
 *                 animation automatically. You may want to use this version if you want to drive the animation
 *                 from your own Animatable or via events such as download progress or a gesture.
 * @param outlineMasksAndMattes Enable this to debug slow animations by outlining masks and mattes.
 *                              The performance overhead of the masks and mattes will be proportional to the
 *                              surface area of all of the masks/mattes combined.
 *                              DO NOT leave this enabled in production.
 * @param applyOpacityToLayers Sets whether to apply opacity to the each layer instead of shape.
 *                             Opacity is normally applied directly to a shape. In cases where translucent
 *                             shapes overlap, applying opacity to a layer will be more accurate at the
 *                             expense of performance.
 *                             Note: This process is very expensive. The performance impact will be reduced
 *                             when hardware acceleration is enabled.
 * @param enableMergePaths Enables experimental merge paths support. Most animations with merge paths will
 *                         want this on but merge path support is more limited than some other rendering
 *                         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 renderMode Allows you to specify whether you want Lottie to use hardware or software rendering.
 *                   Defaults to AUTOMATIC. Refer to [LottieAnimationView.setRenderMode] for more info.
 * @param maintainOriginalImageBounds When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation,
 *                                    regardless of the bitmap size.
 *                                    When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
 *                                    Defaults to false.
 * @param dynamicProperties Allows you to change the properties of an animation dynamically. To use them, use
 *                          [rememberLottieDynamicProperties]. Refer to its docs for more info.
 * @param alignment Define where the animation should be placed within this composable if it has a different
 *                  size than this composable.
 * @param contentScale Define how the animation should be scaled if it has a different size than this Composable.
 * @param clipToCompositionBounds Determines whether or not Lottie will clip the animation to the original animation composition bounds.
 */
@Composable
fun LottieAnimation(
    composition: LottieComposition?,
    @FloatRange(from = 0.0, to = 1.0) progress: Float,
    modifier: Modifier = Modifier,
    outlineMasksAndMattes: Boolean = false,
    applyOpacityToLayers: Boolean = false,
    enableMergePaths: Boolean = false,
    renderMode: RenderMode = RenderMode.AUTOMATIC,
    maintainOriginalImageBounds: Boolean = false,
    dynamicProperties: LottieDynamicProperties? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    clipToCompositionBounds: Boolean = true,
) {
    val drawable = remember { LottieDrawable() }
    val matrix = remember { Matrix() }
    var setDynamicProperties: LottieDynamicProperties? by remember { mutableStateOf(null) }

    if (composition == null || composition.duration == 0f) return Box(modifier)

    val dpScale = Utils.dpScale()
    Canvas(
        modifier = modifier
            .size((composition.bounds.width() / dpScale).dp, (composition.bounds.height() / dpScale).dp)
    ) {
        drawIntoCanvas { canvas ->
            val compositionSize = Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat())
            val intSize = IntSize(size.width.roundToInt(), size.height.roundToInt())

            val scale = contentScale.computeScaleFactor(compositionSize, size)
            val translation = alignment.align(compositionSize * scale, intSize, layoutDirection)
            matrix.reset()
            matrix.preTranslate(translation.x.toFloat(), translation.y.toFloat())
            matrix.preScale(scale.scaleX, scale.scaleY)

            drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
            drawable.renderMode = renderMode
            drawable.composition = composition
            if (dynamicProperties !== setDynamicProperties) {
                setDynamicProperties?.removeFrom(drawable)
                dynamicProperties?.addTo(drawable)
                setDynamicProperties = dynamicProperties
            }
            drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
            drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
            drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
            drawable.clipToCompositionBounds = clipToCompositionBounds
            drawable.progress = progress
            drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
            drawable.draw(canvas.nativeCanvas, matrix)
        }
    }
}

/**
 * This is like [LottieAnimation] except that it handles driving the animation via [animateLottieCompositionAsState]
 * instead of taking a raw progress parameter.
 *
 * @see LottieAnimation
 * @see animateLottieCompositionAsState
 */
@Composable
fun LottieAnimation(
    composition: LottieComposition?,
    modifier: Modifier = Modifier,
    isPlaying: Boolean = true,
    restartOnPlay: Boolean = true,
    clipSpec: LottieClipSpec? = null,
    speed: Float = 1f,
    iterations: Int = 1,
    outlineMasksAndMattes: Boolean = false,
    applyOpacityToLayers: Boolean = false,
    enableMergePaths: Boolean = false,
    renderMode: RenderMode = RenderMode.AUTOMATIC,
    maintainOriginalImageBounds: Boolean = false,
    dynamicProperties: LottieDynamicProperties? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    clipToCompositionBounds: Boolean = true,
) {
    val progress by animateLottieCompositionAsState(
        composition,
        isPlaying,
        restartOnPlay,
        clipSpec,
        speed,
        iterations,
    )
    LottieAnimation(
        composition,
        progress,
        modifier,
        outlineMasksAndMattes,
        applyOpacityToLayers,
        enableMergePaths,
        renderMode,
        maintainOriginalImageBounds,
        dynamicProperties,
        alignment,
        contentScale,
        clipToCompositionBounds,
    )
}

private operator fun Size.times(scale: ScaleFactor): IntSize {
    return IntSize((width * scale.scaleX).toInt(), (height * scale.scaleY).toInt())
}

package com.airbnb.lottie.compose

import androidx.compose.animation.core.AnimationConstants
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import com.airbnb.lottie.LottieComposition

/**
 * [LottieAnimationState] is a value holder that contains information about the current Lottie animation.
 *
 * The primary values are [LottieAnimationState.progress] and [LottieAnimationState.composition]. These
 * value should be passed into the main [LottieAnimation] composable.
 *
 * @see progress
 * @see composition
 * @see animateLottieCompositionAsState
 */
@Stable
interface LottieAnimationState : State<Float> {
    val isPlaying: Boolean

    val progress: Float

    val iteration: Int

    val iterations: Int

    val clipSpec: LottieClipSpec?

    val speed: Float

    val composition: LottieComposition?

    val lastFrameNanos: Long get() = AnimationConstants.UnspecifiedTime

    val isAtEnd: Boolean
}

package com.airbnb.lottie.compose

/**
 * Determines how the animation should behave if the calling CoroutineScope is cancelled.
 *
 * @see rememberLottieAnimatable
 */
enum class LottieCancellationBehavior {
    /**
     * Stop animation immediately and return early.
     */
    Immediately,

    /**
     * Delay cancellations until the current iteration has fully completed.
     * This can be useful in state based transitions where you want one animation to finish its
     * animation before continuing to the next.
     */
    OnIterationFinish,
}

package com.airbnb.lottie.compose

import com.airbnb.lottie.LottieComposition

/**
 * Use subclasses of [LottieClipSpec] to set min/max bounds on the animation playback.
 *
 * @see LottieAnimation
 * @see rememberLottieAnimatable
 * @see animateLottieCompositionAsState
 */
sealed class LottieClipSpec {

    internal abstract fun getMinProgress(composition: LottieComposition): Float

    internal abstract fun getMaxProgress(composition: LottieComposition): Float

    /**
     * Play the animation between these two frames. [maxInclusive] determines whether the animation
     * should play the max frame or stop one frame before it.
     */
    data class Frame(
        val min: Int? = null,
        val max: Int? = null,
        val maxInclusive: Boolean = true,
    ) : LottieClipSpec() {

        private val actualMaxFrame = when {
            max == null -> null
            maxInclusive -> max
            else -> max - 1
        }

        override fun getMinProgress(composition: LottieComposition): Float {
            return when (min) {
                null -> 0f
                else -> (min / composition.endFrame).coerceIn(0f, 1f)
            }
        }

        override fun getMaxProgress(composition: LottieComposition): Float {
            return when (actualMaxFrame) {
                null -> 1f
                else -> (actualMaxFrame / composition.endFrame).coerceIn(0f, 1f)
            }
        }
    }

    /**
     * Play the animation between these two progress values.
     */
    data class Progress(
        val min: Float = 0f,
        val max: Float = 1f,
    ) : LottieClipSpec() {
        override fun getMinProgress(composition: LottieComposition): Float {
            return min
        }

        override fun getMaxProgress(composition: LottieComposition): Float {
            return max
        }
    }

    /**
     * Play the animation from minMarker until maxMarker. If maxMarker represents the end of your animation,
     * set [maxInclusive] to true. If the marker represents the beginning of the next section, set
     * it to false to stop the animation at the frame before maxMarker.
     */
    data class Markers(
        val min: String? = null,
        val max: String? = null,
        val maxInclusive: Boolean = true
    ) : LottieClipSpec() {
        override fun getMinProgress(composition: LottieComposition): Float {
            return when (min) {
                null -> 0f
                else -> ((composition.getMarker(min)?.startFrame ?: 0f) / composition.endFrame).coerceIn(0f, 1f)
            }
        }

        override fun getMaxProgress(composition: LottieComposition): Float {
            return when (max) {
                null -> 1f
                else -> {
                    val offset = if (maxInclusive) 0 else -1
                    return ((composition.getMarker(max)?.startFrame?.plus(offset) ?: 0f) / composition.endFrame).coerceIn(0f, 1f)
                }
            }
        }
    }

    /**
     * Play the animation from the beginning of the marker for the duration of the marker itself.
     * The duration can be set in After Effects.
     */
    data class Marker(val marker: String) : LottieClipSpec() {
        override fun getMinProgress(composition: LottieComposition): Float {
            return ((composition.getMarker(marker)?.startFrame ?: 0f) / composition.endFrame).coerceIn(0f, 1f)
        }

        override fun getMaxProgress(composition: LottieComposition): Float {
            val marker = composition.getMarker(marker) ?: return 1f
            return ((marker.startFrame + marker.durationFrames) / composition.endFrame).coerceIn(0f, 1f)
        }
    }
}

package com.airbnb.lottie.compose

import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.airbnb.lottie.LottieComposition
import kotlinx.coroutines.CompletableDeferred

/**
 * A [LottieCompositionResult] subclass is returned from [rememberLottieComposition].
 * It can be completed with a result or exception only one time.
 *
 * This class implements State<LottieComposition> so you either use it like:
 * ```
 * val compositionResult = rememberLottieComposition(...)
 * // Or
 * val composition by rememberLottieComposition(...)
 * ```
 *
 * Use the former if you need to explicitly differentiate between loading and error states
 * or if you need to call [await] or [awaitOrNull] in a coroutine such as [androidx.compose.runtime.LaunchedEffect].
 *
 * @see rememberLottieComposition
 * @see LottieAnimation
 */
@Stable
interface LottieCompositionResult : State<LottieComposition?> {
    /**
     * The composition or null if it hasn't yet loaded or failed to load.
     */
    override val value: LottieComposition?

    /**
     * The exception that was thrown while trying to load and parse the composition.
     */
    val error: Throwable?

    /**
     * Whether or not the composition is still being loaded and parsed.
     */
    val isLoading: Boolean

    /**
     * Whether or not the composition is in the process of being loaded or parsed.
     */
    val isComplete: Boolean

    /**
     * Whether or not the composition failed to load. This is terminal. It only occurs after
     * returning false from [rememberLottieComposition]'s onRetry lambda.
     */
    val isFailure: Boolean

    /**
     * Whether or not the composition has succeeded yet.
     */
    val isSuccess: Boolean

    /**
     * Suspend until the composition has finished parsing.
     *
     * This can throw if the [LottieComposition] fails to load.
     *
     * These animations should never fail given a valid input:
     * * [LottieCompositionSpec.RawRes]
     * * [LottieCompositionSpec.Asset]
     * * [LottieCompositionSpec.JsonString]
     *
     * These animations may fail:
     * * [LottieCompositionSpec.Url]
     * * [LottieCompositionSpec.File]
     */
    suspend fun await(): LottieComposition
}

/**
 * Like [LottieCompositionResult.await] but returns null instead of throwing an exception if the animation fails
 * to load.
 */
suspend fun LottieCompositionResult.awaitOrNull(): LottieComposition? {
    return try {
        await()
    } catch (e: Throwable) {
        null
    }
}

@Stable
internal class LottieCompositionResultImpl : LottieCompositionResult {
    private val compositionDeferred = CompletableDeferred<LottieComposition>()

    override var value: LottieComposition? by mutableStateOf(null)
        private set

    override var error by mutableStateOf<Throwable?>(null)
        private set

    override val isLoading by derivedStateOf { value == null && error == null }

    override val isComplete by derivedStateOf { value != null || error != null }

    override val isFailure by derivedStateOf { error != null }

    override val isSuccess by derivedStateOf { value != null }

    override suspend fun await(): LottieComposition {
        return compositionDeferred.await()
    }

    @Synchronized
    internal fun complete(composition: LottieComposition) {
        if (isComplete) return

        this.value = composition
        compositionDeferred.complete(composition)
    }

    @Synchronized
    internal fun completeExceptionally(error: Throwable) {
        if (isComplete) return

        this.error = error
        compositionDeferred.completeExceptionally(error)
    }
}

package com.airbnb.lottie.compose

import android.net.Uri

/**
 * Specification for a [com.airbnb.lottie.LottieComposition]. Each subclass represents a different source.
 * A [com.airbnb.lottie.LottieComposition] is the stateless parsed version of a Lottie json file and is
 * passed into [rememberLottieComposition] or [LottieAnimation].
 */
sealed interface LottieCompositionSpec {

    /**
     * Load an animation from res/raw.
     */
    @JvmInline
    value class RawRes(@androidx.annotation.RawRes val resId: Int) : LottieCompositionSpec

    /**
     * Load an animation from the internet. Lottie has a default network stack that will use
     * standard Android networking APIs to attempt to load your animation. You may want to
     * integrate your own networking stack instead for consistency, to add your own headers,
     * or implement retries. To do that, call [com.airbnb.lottie.Lottie.initialize] and set
     * a [com.airbnb.lottie.network.LottieNetworkFetcher] on the [com.airbnb.lottie.LottieConfig].
     *
     * If you are using this spec, you may want to use [rememberLottieComposition] instead of
     * passing this spec directly into [LottieAnimation] because it can fail and you want to
     * make sure that you properly handle the failures and/or retries.
     */
    @JvmInline
    value class Url(val url: String) : LottieCompositionSpec

    /**
     * Load an animation from an arbitrary file. Make sure that your app has permissions to read it
     * or else this may fail.
     */
    @JvmInline
    value class File(val fileName: String) : LottieCompositionSpec

    /**
     * Load an animation from the assets directory of your app. This isn't type safe like [RawRes]
     * so make sure that the path to your animation is correct this will fail.
     */
    @JvmInline
    value class Asset(val assetName: String) : LottieCompositionSpec

    /**
     * Load an animation from its json string.
     */
    @JvmInline
    value class JsonString(val jsonString: String) : LottieCompositionSpec

    /**
     * Load an animation from a content provider URI.
     */
    @JvmInline
    value class ContentProvider(val uri: Uri) : LottieCompositionSpec
}

package com.airbnb.lottie.compose

import android.content.Context

object LottieConstants {
    /**
     * Use this with [animateLottieCompositionAsState]'s iterations parameter to repeat forever.
     */
    const val IterateForever = Integer.MAX_VALUE
}

package com.airbnb.lottie.compose

import android.graphics.Bitmap
import android.graphics.ColorFilter
import android.graphics.PointF
import android.graphics.Typeface
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
 * @see LottieDrawable.setMaintainOriginalImageBounds
 */
@Composable
fun rememberLottieDynamicProperties(
    vararg properties: LottieDynamicProperty<*>,
): LottieDynamicProperties {
    return remember(properties.contentHashCode()) {
        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.contentHashCode()) { 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>>,
    // Java doesn't have reified types. All LottieProperty arrays are Integer[].
    private val intArrayProperties: List<LottieDynamicProperty<Array<*>>>,
    private val typefaceProperties: List<LottieDynamicProperty<Typeface>>,
    private val bitmapProperties: List<LottieDynamicProperty<Bitmap>>,
    private val charSequenceProperties: List<LottieDynamicProperty<CharSequence>>,
) {
    @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 Array<*> } as List<LottieDynamicProperty<Array<*>>>,
        properties.filter { it.property is Typeface } as List<LottieDynamicProperty<Typeface>>,
        properties.filter { it.property is Bitmap } as List<LottieDynamicProperty<Bitmap>>,
        properties.filter { it.property is CharSequence } as List<LottieDynamicProperty<CharSequence>>,
    )

    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())
        }
        typefaceProperties.forEach { p ->
            drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
        }
        bitmapProperties.forEach { p ->
            drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
        }
        charSequenceProperties.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<Array<*>>?)
        }
        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>?)
        }
        charSequenceProperties.forEach { p ->
            drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<CharSequence>?)
        }
    }
}

private fun <T> ((frameInfo: LottieFrameInfo<T>) -> T).toValueCallback() = object : LottieValueCallback<T>() {
    override fun getValue(frameInfo: LottieFrameInfo<T>): T {
        return invoke(frameInfo)
    }
}

package com.airbnb.lottie.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking

/**
 * @see LottieRetrySignal
 * @see rememberLottieComposition
 */
@Composable
fun rememberLottieRetrySignal(): LottieRetrySignal {
    return remember { LottieRetrySignal() }
}

/**
 * Helper to retry compositions that fail to load. This will mostly happen for animations loaded via url.
 *
 * Call [retry] from an action that should trigger a retry such as a snackbar or retry button.
 *
 * Call [awaitRetry] from the onRetry lambda to [rememberLottieComposition] then return true.
 *
 * @see rememberLottieComposition
 */
@Stable
class LottieRetrySignal internal constructor() {
    private val channel = Channel<Unit>(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    var isAwaitingRetry: Boolean by mutableStateOf(false)
        private set

    fun retry() {
        channel.trySendBlocking(Unit)
    }

    suspend fun awaitRetry() {
        try {
            isAwaitingRetry = true
            channel.receive()
        } finally {
            isAwaitingRetry = false
        }
    }
}

package com.airbnb.lottie.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.utils.Utils

/**
 * Returns a [LottieAnimationState] representing the progress of an animation.
 *
 * This is the declarative version of [rememberLottieAnimatable] and [LottieAnimation].
 *
 * @param composition The composition to render. This should be retrieved with [rememberLottieComposition].
 * @param isPlaying Whether or not the animation is currently playing. Note that the internal
 *                  animation may end due to reaching the target iterations count. If that happens,
 *                  the animation may stop even if this is still true. You can observe the returned
 *                  [LottieAnimationState.isPlaying] to determine whether the underlying animation
 *                  is still playing.
 * @param restartOnPlay If isPlaying switches from false to true, restartOnPlay determines whether
 *                      the progress and iteration gets reset.
 * @param clipSpec A [LottieClipSpec] that specifies the bound the animation playback
 *                 should be clipped to.
 * @param speed The speed the animation should play at. Numbers larger than one will speed it up.
 *              Numbers between 0 and 1 will slow it down. Numbers less than 0 will play it backwards.
 * @param iterations The number of times the animation should repeat before stopping. It must be
 *                    a positive number. [LottieConstants.IterateForever] can be used to repeat forever.
 * @param cancellationBehavior The behavior that this animation should have when cancelled. In most cases,
 *                             you will want it to cancel immediately. However, if you have a state based
 *                             transition and you want an animation to finish playing before moving on to
 *                             the next one then you may want to set this to [LottieCancellationBehavior.OnIterationFinish].
 * @param ignoreSystemAnimatorScale By default, Lottie will respect the system animator scale set in developer options or set to 0
 *                                  by things like battery saver mode. When set to 0, the speed will effectively become [Integer.MAX_VALUE].
 *                                  Set this to false if you want to ignore the system animator scale and always default to normal speed.
 */
@Composable
fun animateLottieCompositionAsState(
    composition: LottieComposition?,
    isPlaying: Boolean = true,
    restartOnPlay: Boolean = true,
    clipSpec: LottieClipSpec? = null,
    speed: Float = 1f,
    iterations: Int = 1,
    cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediately,
    ignoreSystemAnimatorScale: Boolean = false,
): LottieAnimationState {
    require(iterations > 0) { "Iterations must be a positive number ($iterations)." }
    require(speed.isFinite()) { "Speed must be a finite number. It is $speed." }

    val animatable = rememberLottieAnimatable()
    var wasPlaying by remember { mutableStateOf(isPlaying) }

    // Dividing by 0 correctly yields Float.POSITIVE_INFINITY here.
    val actualSpeed = if (ignoreSystemAnimatorScale) speed else (speed / Utils.getAnimationScale(LocalContext.current))

    LaunchedEffect(
        composition,
        isPlaying,
        clipSpec,
        actualSpeed,
        iterations,
    ) {
        if (isPlaying && !wasPlaying && restartOnPlay) {
            animatable.resetToBeginning()
        }
        wasPlaying = isPlaying
        if (!isPlaying) return@LaunchedEffect

        animatable.animate(
            composition,
            iterations = iterations,
            speed = actualSpeed,
            clipSpec = clipSpec,
            initialProgress = animatable.progress,
            continueFromPreviousAnimate = false,
            cancellationBehavior = cancellationBehavior,
        )
    }

    return animatable
}

package com.airbnb.lottie.compose

import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Typeface
import android.util.Base64
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieImageAsset
import com.airbnb.lottie.LottieTask
import com.airbnb.lottie.model.Font
import com.airbnb.lottie.utils.Logger
import com.airbnb.lottie.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.IOException
import java.util.zip.ZipInputStream
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * Use this with [rememberLottieComposition#cacheKey]'s cacheKey parameter to generate a default
 * cache key for the composition.
 */
private const val DefaultCacheKey = "__LottieInternalDefaultCacheKey__"

/**
 * Takes a [LottieCompositionSpec], attempts to load and parse the animation, and returns a [LottieCompositionResult].
 *
 * [LottieCompositionResult] allows you to explicitly check for loading, failures, call
 * [LottieCompositionResult.await], or invoke it like a function to get the nullable composition.
 *
 * [LottieCompositionResult] implements State<LottieComposition?> so if you don't need the full result class,
 * you can use this function like:
 * ```
 * val compositionResult: LottieCompositionResult = lottieComposition(spec)
 * // or...
 * val composition: State<LottieComposition?> by lottieComposition(spec)
 * ```
 *
 * The loaded composition will automatically load and set images that are embedded in the json as a base64 string
 * or will load them from assets if an imageAssetsFolder is supplied.
 *
 * @param spec The [LottieCompositionSpec] that defines which LottieComposition should be loaded.
 * @param imageAssetsFolder A subfolder in `src/main/assets` that contains the exported images
 *                          that this composition uses. DO NOT rename any images from your design tool. The
 *                          filenames must match the values that are in your json file.
 * @param fontAssetsFolder The default folder Lottie will look in to find font files. Fonts will be matched
 *                         based on the family name specified in the Lottie json file.
 *                         Defaults to "fonts/" so if "Helvetica" was in the Json file, Lottie will auto-match
 *                         fonts located at "src/main/assets/fonts/Helvetica.ttf". Missing fonts will be skipped
 *                         and should be set via fontRemapping or via dynamic properties.
 * @param fontFileExtension The default file extension for font files specified in the fontAssetsFolder or fontRemapping.
 *                          Defaults to ttf.
 * @param cacheKey Set a cache key for this composition. When set, subsequent calls to fetch this composition will
 *                 return directly from the cache instead of having to reload and parse the animation. Set this to
 *                 null to skip the cache. By default, this will automatically generate a cache key derived
 *                 from your [LottieCompositionSpec].
 * @param onRetry An optional callback that will be called if loading the animation fails.
 *                It is passed the failed count (the number of times it has failed) and the exception
 *                from the previous attempt to load the composition. [onRetry] is a suspending function
 *                so you can do things like add a backoff delay or await an internet connection before
 *                retrying again. [rememberLottieRetrySignal] can be used to handle explicit retires.
 */
@Composable
fun rememberLottieComposition(
    spec: LottieCompositionSpec,
    imageAssetsFolder: String? = null,
    fontAssetsFolder: String = "fonts/",
    fontFileExtension: String = ".ttf",
    cacheKey: String? = DefaultCacheKey,
    onRetry: suspend (failCount: Int, previousException: Throwable) -> Boolean = { _, _ -> false },
): LottieCompositionResult {
    val context = LocalContext.current
    val result by remember(spec) { mutableStateOf(LottieCompositionResultImpl()) }
    // Warm the task cache. We can start the parsing task before the LaunchedEffect gets dispatched and run.
    // The LaunchedEffect task will join the task created inline here via LottieCompositionFactory's task cache.
    remember(spec, cacheKey) { lottieTask(context, spec, cacheKey, isWarmingCache = true) }
    LaunchedEffect(spec, cacheKey) {
        var exception: Throwable? = null
        var failedCount = 0
        while (!result.isSuccess && (failedCount == 0 || onRetry(failedCount, exception!!))) {
            try {
                val composition = lottieComposition(
                    context,
                    spec,
                    imageAssetsFolder.ensureTrailingSlash(),
                    fontAssetsFolder.ensureTrailingSlash(),
                    fontFileExtension.ensureLeadingPeriod(),
                    cacheKey,
                )
                result.complete(composition)
            } catch (e: Throwable) {
                exception = e
                failedCount++
            }
        }
        if (!result.isComplete && exception != null) {
            result.completeExceptionally(exception)
        }
    }
    return result
}

private suspend fun lottieComposition(
    context: Context,
    spec: LottieCompositionSpec,
    imageAssetsFolder: String?,
    fontAssetsFolder: String?,
    fontFileExtension: String,
    cacheKey: String?,
): LottieComposition {
    val task = requireNotNull(lottieTask(context, spec, cacheKey, isWarmingCache = false)) {
        "Unable to create parsing task for $spec."
    }

    val composition = task.await()
    loadImagesFromAssets(context, composition, imageAssetsFolder)
    loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension)
    return composition
}

private fun lottieTask(
    context: Context,
    spec: LottieCompositionSpec,
    cacheKey: String?,
    isWarmingCache: Boolean,
): LottieTask<LottieComposition>? {
    return when (spec) {
        is LottieCompositionSpec.RawRes -> {
            if (cacheKey == DefaultCacheKey) {
                LottieCompositionFactory.fromRawRes(context, spec.resId)
            } else {
                LottieCompositionFactory.fromRawRes(context, spec.resId, cacheKey)
            }
        }
        is LottieCompositionSpec.Url -> {
            if (cacheKey == DefaultCacheKey) {
                LottieCompositionFactory.fromUrl(context, spec.url)
            } else {
                LottieCompositionFactory.fromUrl(context, spec.url, cacheKey)
            }
        }
        is LottieCompositionSpec.File -> {
            if (isWarmingCache) {
                // Warming the cache is done from the main thread so we can't
                // create the FileInputStream needed in this path.
                null
            } else {
                val fis = FileInputStream(spec.fileName)
                when {
                    spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
                        ZipInputStream(fis),
                        if (cacheKey == DefaultCacheKey) spec.fileName else cacheKey,
                    )
                    else -> LottieCompositionFactory.fromJsonInputStream(
                        fis,
                        if (cacheKey == DefaultCacheKey) spec.fileName else cacheKey,
                    )
                }
            }
        }
        is LottieCompositionSpec.Asset -> {
            if (cacheKey == DefaultCacheKey) {
                LottieCompositionFactory.fromAsset(context, spec.assetName)
            } else {
                LottieCompositionFactory.fromAsset(context, spec.assetName, cacheKey)
            }
        }
        is LottieCompositionSpec.JsonString -> {
            val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey
            LottieCompositionFactory.fromJsonString(spec.jsonString, jsonStringCacheKey)
        }
        is LottieCompositionSpec.ContentProvider -> {
            val inputStream = context.contentResolver.openInputStream(spec.uri)
            LottieCompositionFactory.fromJsonInputStream(inputStream, if (cacheKey == DefaultCacheKey) spec.uri.toString() else cacheKey)
        }
    }
}

private suspend fun <T> LottieTask<T>.await(): T = suspendCancellableCoroutine { cont ->
    addListener { c ->
        if (!cont.isCompleted) cont.resume(c)
    }.addFailureListener { e ->
        if (!cont.isCompleted) cont.resumeWithException(e)
    }
}

private suspend fun loadImagesFromAssets(
    context: Context,
    composition: LottieComposition,
    imageAssetsFolder: String?,
) {
    if (!composition.hasImages()) {
        return
    }
    withContext(Dispatchers.IO) {
        for (asset in composition.images.values) {
            maybeDecodeBase64Image(asset)
            maybeLoadImageFromAsset(context, asset, imageAssetsFolder)
        }
    }
}

private fun maybeLoadImageFromAsset(
    context: Context,
    asset: LottieImageAsset,
    imageAssetsFolder: String?,
) {
    if (asset.bitmap != null || imageAssetsFolder == null) return
    val filename = asset.fileName
    val inputStream = try {
        context.assets.open(imageAssetsFolder + filename)
    } catch (e: IOException) {
        Logger.warning("Unable to open asset.", e)
        return
    }
    try {
        val opts = BitmapFactory.Options()
        opts.inScaled = true
        opts.inDensity = 160
        var bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
        bitmap = Utils.resizeBitmapIfNeeded(bitmap, asset.width, asset.height)
        asset.bitmap = bitmap
    } catch (e: IllegalArgumentException) {
        Logger.warning("Unable to decode image.", e)
    }
}

private fun maybeDecodeBase64Image(asset: LottieImageAsset) {
    if (asset.bitmap != null) return
    val filename = asset.fileName
    if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
        // Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
        try {
            val data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT)
            val opts = BitmapFactory.Options()
            opts.inScaled = true
            opts.inDensity = 160
            asset.bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opts)
        } catch (e: IllegalArgumentException) {
            Logger.warning("data URL did not have correct base64 format.", e)
        }
    }
}

private suspend fun loadFontsFromAssets(
    context: Context,
    composition: LottieComposition,
    fontAssetsFolder: String?,
    fontFileExtension: String,
) {
    if (composition.fonts.isEmpty()) return
    withContext(Dispatchers.IO) {
        for (font in composition.fonts.values) {
            maybeLoadTypefaceFromAssets(context, font, fontAssetsFolder, fontFileExtension)
        }
    }
}

private fun maybeLoadTypefaceFromAssets(
    context: Context,
    font: Font,
    fontAssetsFolder: String?,
    fontFileExtension: String,
) {
    val path = "$fontAssetsFolder${font.family}${fontFileExtension}"
    val typefaceWithDefaultStyle = try {
        Typeface.createFromAsset(context.assets, path)
    } catch (e: Exception) {
        Logger.error("Failed to find typeface in assets with path $path.", e)
        return
    }
    try {
        val typefaceWithStyle = typefaceForStyle(typefaceWithDefaultStyle, font.style)
        font.typeface = typefaceWithStyle
    } catch (e: Exception) {
        Logger.error("Failed to create ${font.family} typeface with style=${font.style}!", e)
    }
}

private fun typefaceForStyle(typeface: Typeface, style: String): Typeface? {
    val containsItalic = style.contains("Italic")
    val containsBold = style.contains("Bold")
    val styleInt = when {
        containsItalic && containsBold -> Typeface.BOLD_ITALIC
        containsItalic -> Typeface.ITALIC
        containsBold -> Typeface.BOLD
        else -> Typeface.NORMAL
    }
    return if (typeface.style == styleInt) typeface else Typeface.create(typeface, styleInt)
}

private fun String?.ensureTrailingSlash(): String? = when {
    isNullOrBlank() -> null
    endsWith('/') -> this
    else -> "$this/"
}

private fun String.ensureLeadingPeriod(): String = when {
    isBlank() -> this
    startsWith(".") -> this
    else -> ".$this"
}

package com.airbnb.lottie.samples

import android.animation.Animator

internal open class AnimatorListenerAdapter(
    val onStart: ((Animator) -> Unit)? = null,
    val onRepeat: ((Animator) -> Unit)? = null,
    val onEnd: ((Animator) -> Unit)? = null,
    val onCancel: ((Animator) -> Unit)? = null
) : Animator.AnimatorListener {

    override fun onAnimationStart(animation: Animator) = onStart?.invoke(animation) ?: Unit
    override fun onAnimationRepeat(animation: Animator) = onRepeat?.invoke(animation) ?: Unit
    override fun onAnimationEnd(animation: Animator) = onEnd?.invoke(animation) ?: Unit
    override fun onAnimationCancel(animation: Animator) = onCancel?.invoke(animation) ?: Unit
}

package com.airbnb.lottie.samples

import android.graphics.PointF
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.customview.widget.ViewDragHelper
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import com.airbnb.lottie.samples.databinding.BullseyeActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding
import com.airbnb.lottie.value.LottieRelativePointValueCallback

class BullseyeActivity : AppCompatActivity() {
    private val binding: BullseyeActivityBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val largeValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
        binding.animationView.addValueCallback(KeyPath("First"), LottieProperty.TRANSFORM_POSITION, largeValueCallback)

        val mediumValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
        binding.animationView.addValueCallback(KeyPath("Fourth"), LottieProperty.TRANSFORM_POSITION, mediumValueCallback)

        val smallValueCallback = LottieRelativePointValueCallback(PointF(0f, 0f))
        binding.animationView.addValueCallback(KeyPath("Seventh"), LottieProperty.TRANSFORM_POSITION, smallValueCallback)

        var totalDx = 0f
        var totalDy = 0f

        val viewDragHelper = ViewDragHelper.create(binding.containerView, object : ViewDragHelper.Callback() {
            override fun tryCaptureView(child: View, pointerId: Int) = child == binding.targetView

            override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
                return top
            }

            override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
                return left
            }

            override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
                totalDx += dx
                totalDy += dy
                smallValueCallback.setValue(getPoint(totalDx, totalDy, 1.2f))
                mediumValueCallback.setValue(getPoint(totalDx, totalDy, 1f))
                largeValueCallback.setValue(getPoint(totalDx, totalDy, 0.75f))
            }
        })

        binding.containerView.viewDragHelper = viewDragHelper
    }

    private fun getPoint(dx: Float, dy: Float, factor: Float) = PointF(dx * factor, dy * factor)
}

package com.airbnb.lottie.samples

import android.annotation.SuppressLint
import android.graphics.PointF
import android.os.Bundle
import android.util.Log
import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import com.airbnb.lottie.samples.databinding.DynamicActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding

private val COLORS = arrayOf(
    0xff5a5f,
    0x008489,
    0xa61d55
)
private val EXTRA_JUMP = arrayOf(0f, 20f, 50f)

class DynamicActivity : AppCompatActivity() {
    private val binding: DynamicActivityBinding by viewBinding()

    private var speed = 1
    private var colorIndex = 0
    private var extraJumpIndex = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.speedButton.setOnClickListener {
            speed = ++speed % 4
            updateButtonText()
        }

        binding.colorButton.setOnClickListener {
            colorIndex = (colorIndex + 1) % COLORS.size
            updateButtonText()
        }

        binding.jumpHeight.setOnClickListener {
            extraJumpIndex = (extraJumpIndex + 1) % EXTRA_JUMP.size
            updateButtonText()
            setupValueCallbacks()
        }

        binding.animationView.addLottieOnCompositionLoadedListener { _ ->
            binding.animationView.resolveKeyPath(KeyPath("**")).forEach {
                Log.d(TAG, it.keysToString())
                setupValueCallbacks()
            }
        }
        binding.animationView.setFailureListener { e ->
            Log.e(TAG, "Failed to load animation!", e)
        }

        updateButtonText()
    }

    private fun setupValueCallbacks() {
        binding.animationView.addValueCallback(KeyPath("LeftArmWave"), LottieProperty.TIME_REMAP) { frameInfo ->
            2 * speed.toFloat() * frameInfo.overallProgress
        }

        val shirt = KeyPath("Shirt", "Group 5", "Fill 1")
        val leftArm = KeyPath("LeftArmWave", "LeftArm", "Group 6", "Fill 1")
        val rightArm = KeyPath("RightArm", "Group 6", "Fill 1")

        binding.animationView.addValueCallback(shirt, LottieProperty.COLOR) { COLORS[colorIndex] }
        binding.animationView.addValueCallback(leftArm, LottieProperty.COLOR) { COLORS[colorIndex] }
        binding.animationView.addValueCallback(rightArm, LottieProperty.COLOR) { COLORS[colorIndex] }
        val point = PointF()
        binding.animationView.addValueCallback(
            KeyPath("Body"),
            LottieProperty.TRANSFORM_POSITION
        ) { frameInfo ->
            val startX = frameInfo.startValue.x
            var startY = frameInfo.startValue.y
            var endY = frameInfo.endValue.y

            if (startY > endY) {
                startY += EXTRA_JUMP[extraJumpIndex]
            } else if (endY > startY) {
                endY += EXTRA_JUMP[extraJumpIndex]
            }
            point.set(startX, lerp(startY, endY, frameInfo.interpolatedKeyframeProgress))
            point
        }
    }

    @SuppressLint("SetTextI18n")
    private fun updateButtonText() {
        binding.speedButton.text = "Wave: ${speed}x Speed"
        binding.jumpHeight.text = "Extra jump height ${EXTRA_JUMP[extraJumpIndex]}"
    }

    fun lerp(a: Float, b: Float, @FloatRange(from = 0.0, to = 1.0) percentage: Float) = a + percentage * (b - a)

    companion object {
        val TAG = DynamicActivity::class.simpleName
    }
}

package com.airbnb.lottie.samples

import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.TextDelegate
import com.airbnb.lottie.samples.databinding.DynamicTextActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding

class DynamicTextActivity : AppCompatActivity() {
    private val binding: DynamicTextActivityBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val textDelegate = TextDelegate(binding.dynamicTextView)
        binding.nameEditText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                textDelegate.setText("NAME", s.toString())
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        })
        binding.dynamicTextView.setTextDelegate(textDelegate)
    }
}

package com.airbnb.lottie.samples

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.samples.databinding.EmptyActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding

class EmptyActivity : AppCompatActivity() {
    private val binding: EmptyActivityBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.finish.setOnClickListener { finish() }
    }
}

package com.airbnb.lottie.samples

import androidx.multidex.MultiDexApplication
import com.airbnb.lottie.L
import com.airbnb.lottie.samples.api.LottiefilesApi
import com.google.gson.FieldNamingPolicy
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

class LottieApplication : MultiDexApplication() {
    private val okHttpClient by lazy {
        OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    private val gson by lazy {
        GsonBuilder()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .create()
    }

    private val retrofit by lazy {
        Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl("https://api.lottiefiles.com/")
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
    }

    val lottiefilesService: LottiefilesApi by lazy { retrofit.create(LottiefilesApi::class.java) }

    override fun onCreate() {
        super.onCreate()
        L.DBG = true
        @Suppress("RestrictedApi")
        L.setTraceEnabled(true)
    }
}

package com.airbnb.lottie.samples

import android.content.Context
import android.text.InputType
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.widget.FrameLayout
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable

class LottieFontViewGroup @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val views = ArrayList<View>()

    private val cursorView: LottieAnimationView by lazy { LottieAnimationView(context) }

    init {
        isFocusableInTouchMode = true
        LottieCompositionFactory.fromAsset(context, "Mobilo/BlinkingCursor.json")
            .addListener {
                cursorView.layoutParams = LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
                )
                cursorView.setComposition(it)
                cursorView.repeatCount = LottieDrawable.INFINITE
                cursorView.playAnimation()
                addView(cursorView)
            }
    }

    private fun addSpace() {
        val index = indexOfChild(cursorView)
        addView(createSpaceView(), index)
    }

    override fun addView(child: View, index: Int) {
        super.addView(child, index)
        if (index == -1) {
            views.add(child)
        } else {
            views.add(index, child)
        }
    }

    private fun removeLastView() {
        if (views.size > 1) {
            val position = views.size - 2
            removeView(views[position])
            views.removeAt(position)
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        if (views.isEmpty()) {
            return
        }
        var currentX = paddingTop
        var currentY = paddingLeft

        for (i in views.indices) {
            val view = views[i]
            if (!fitsOnCurrentLine(currentX, view)) {
                if (view.tag != null && view.tag == "Space") {
                    continue
                }
                currentX = paddingLeft
                currentY += view.measuredHeight
            }
            currentX += view.width
        }

        setMeasuredDimension(measuredWidth, currentY + views[views.size - 1].measuredHeight * 2)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        if (views.isEmpty()) {
            return
        }
        var currentX = paddingTop
        var currentY = paddingLeft

        for (i in views.indices) {
            val view = views[i]
            if (!fitsOnCurrentLine(currentX, view)) {
                if (view.tag != null && view.tag == "Space") {
                    continue
                }
                currentX = paddingLeft
                currentY += view.measuredHeight
            }
            view.layout(
                currentX, currentY, currentX + view.measuredWidth,
                currentY + view.measuredHeight
            )
            currentX += view.width
        }
    }

    override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
        val fic = BaseInputConnection(this, false)
        outAttrs.actionLabel = null
        outAttrs.inputType = InputType.TYPE_NULL
        outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT
        return fic
    }

    override fun onCheckIsTextEditor(): Boolean {
        return true
    }

    override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
        if (keyCode == KeyEvent.KEYCODE_SPACE) {
            addSpace()
            return true
        }

        if (keyCode == KeyEvent.KEYCODE_DEL) {
            removeLastView()
            return true
        }

        if (!isValidKey(event)) {
            return super.onKeyUp(keyCode, event)
        }

        val letter = "" + Character.toUpperCase(event.unicodeChar.toChar())
        // switch (letter) {
        //     case ",":
        //         letter = "Comma";
        //         break;
        //     case "'":
        //         letter = "Apostrophe";
        //         break;
        //     case ";":
        //     case ":":
        //         letter = "Colon";
        //         break;
        // }
        val fileName = "Mobilo/$letter.json"
        LottieCompositionFactory.fromAsset(context, fileName)
            .addListener { addComposition(it) }

        return true
    }

    private fun isValidKey(event: KeyEvent): Boolean {
        if (!event.hasNoModifiers()) {
            return false
        }
        if (event.keyCode >= KeyEvent.KEYCODE_A && event.keyCode <= KeyEvent.KEYCODE_Z) {
            return true
        }

        // switch (keyCode) {
        //     case KeyEvent.KEYCODE_COMMA:
        //     case KeyEvent.KEYCODE_APOSTROPHE:
        //     case KeyEvent.KEYCODE_SEMICOLON:
        //         return true;
        // }
        return false
    }

    private fun addComposition(composition: LottieComposition) {
        val lottieAnimationView = LottieAnimationView(context)
        lottieAnimationView.layoutParams = LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        lottieAnimationView.setComposition(composition)
        lottieAnimationView.playAnimation()
        val index = indexOfChild(cursorView)
        addView(lottieAnimationView, index)
    }

    private fun fitsOnCurrentLine(currentX: Int, view: View): Boolean {
        return currentX + view.measuredWidth < width - paddingRight
    }

    private fun createSpaceView(): View {
        val spaceView = View(context)
        spaceView.layoutParams = LayoutParams(
            resources.getDimensionPixelSize(R.dimen.font_space_width),
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        spaceView.tag = "Space"
        return spaceView
    }
}

package com.airbnb.lottie.samples

import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingDataAdapter
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.samples.api.LottiefilesApi
import com.airbnb.lottie.samples.databinding.LottiefilesFragmentBinding
import com.airbnb.lottie.samples.model.AnimationData
import com.airbnb.lottie.samples.model.AnimationResponse
import com.airbnb.lottie.samples.model.CompositionArgs
import com.airbnb.lottie.samples.utils.MvRxViewModel
import com.airbnb.lottie.samples.utils.hideKeyboard
import com.airbnb.lottie.samples.utils.viewBinding
import com.airbnb.lottie.samples.views.AnimationItemView
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

data class LottiefilesState(
    val mode: LottiefilesMode = LottiefilesMode.Recent,
    val query: String = ""
) : MvRxState

class LottiefilesViewModel(initialState: LottiefilesState, private val api: LottiefilesApi) : MvRxViewModel<LottiefilesState>(initialState) {

    private var mode = initialState.mode
    private var query = initialState.query

    private var dataSource: LottiefilesDataSource? = null
    val pager = Pager(PagingConfig(pageSize = 16)) {
        LottiefilesDataSource(api, mode, query).also { dataSource = it }
    }.flow.cachedIn(viewModelScope)

    init {
        selectSubscribe(LottiefilesState::mode, LottiefilesState::query) { mode, query ->
            this.mode = mode
            this.query = query
            dataSource?.invalidate()
        }
    }

    fun setMode(mode: LottiefilesMode) = setState { copy(mode = mode) }

    fun setQuery(query: String) = setState { copy(query = query) }

    companion object : MvRxViewModelFactory<LottiefilesViewModel, LottiefilesState> {
        override fun create(viewModelContext: ViewModelContext, state: LottiefilesState): LottiefilesViewModel {
            val service = viewModelContext.app<LottieApplication>().lottiefilesService
            return LottiefilesViewModel(state, service)
        }
    }
}

class LottiefilesDataSource(
    private val api: LottiefilesApi,
    val mode: LottiefilesMode,
    private val query: String
) : PagingSource<Int, AnimationData>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AnimationData> {
        val page = params.key ?: 1
        return try {
            val response = when (mode) {
                LottiefilesMode.Popular -> api.getPopular(page)
                LottiefilesMode.Recent -> api.getRecent(page)
                LottiefilesMode.Search -> {
                    if (query.isBlank()) {
                        AnimationResponse(page, emptyList(), "", page, null, "", 0, "", 0, 0)
                    } else {
                        api.search(query, page)
                    }
                }
            }

            LoadResult.Page(
                response.data,
                if (page == 1) null else page + 1,
                (page + 1).takeIf { page < response.lastPage }
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, AnimationData>): Int? {
        return state.closestItemToPosition(state.anchorPosition ?: return null)?.id as Int?
    }
}

class LottiefilesFragment : BaseMvRxFragment(R.layout.lottiefiles_fragment) {
    private val binding: LottiefilesFragmentBinding by viewBinding()
    private val viewModel: LottiefilesViewModel by fragmentViewModel()

    private object AnimationItemDataDiffCallback : DiffUtil.ItemCallback<AnimationData>() {
        override fun areItemsTheSame(oldItem: AnimationData, newItem: AnimationData) = oldItem.id == newItem.id

        override fun areContentsTheSame(oldItem: AnimationData, newItem: AnimationData) = oldItem == newItem
    }

    private class AnimationItemViewHolder(context: Context) : RecyclerView.ViewHolder(AnimationItemView(context)) {
        fun bind(data: AnimationData?) {
            val view = itemView as AnimationItemView
            view.setTitle(data?.title)
            view.setPreviewUrl(data?.preview)
            view.setPreviewBackgroundColor(data?.bgColorInt)
            view.setOnClickListener {
                val intent = PlayerActivity.intent(view.context, CompositionArgs(animationData = data))
                view.context.startActivity(intent)
            }
        }
    }

    private val adapter = object : PagingDataAdapter<AnimationData, AnimationItemViewHolder>(AnimationItemDataDiffCallback) {
        override fun onBindViewHolder(holder: AnimationItemViewHolder, position: Int) = holder.bind(getItem(position))

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AnimationItemViewHolder(parent.context)

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.recyclerView.adapter = adapter
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.pager.collectLatest(adapter::submitData)
        }
        binding.tabBar.setRecentClickListener {
            viewModel.setMode(LottiefilesMode.Recent)
            requireContext().hideKeyboard()
        }
        binding.tabBar.setPopularClickListener {
            viewModel.setMode(LottiefilesMode.Popular)
            requireContext().hideKeyboard()
        }
        binding.tabBar.setSearchClickListener {
            viewModel.setMode(LottiefilesMode.Search)
            requireContext().hideKeyboard()
        }
        binding.searchView.query.onEach { query ->
            viewModel.setQuery(query)
        }.launchIn(viewLifecycleOwner.lifecycleScope)
    }

    override fun invalidate(): Unit = withState(viewModel) { state ->
        binding.searchView.isVisible = state.mode == LottiefilesMode.Search
        binding.tabBar.setMode(state.mode)
    }
}

package com.airbnb.lottie.samples

enum class LottiefilesMode {
    Popular,
    Recent,
    Search
}

package com.airbnb.lottie.samples

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import com.airbnb.lottie.samples.databinding.MainActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding

class MainActivity : AppCompatActivity() {
    private val binding: MainActivityBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
        super.onCreate(savedInstanceState)
        binding.bottomNavigation.setOnItemSelectedListener listener@{ item ->
            when (item.itemId) {
                R.id.showcase -> showFragment(ShowcaseFragment())
                R.id.preview -> showFragment(PreviewFragment())
                R.id.lottiefiles -> showFragment(LottiefilesFragment())
                R.id.learn -> showShowcase()
            }
            true
        }

        if (savedInstanceState == null) {
            showFragment(ShowcaseFragment())
        }
    }

    private fun showShowcase() {
        val intent = CustomTabsIntent.Builder().build()
        intent.launchUrl(this, "http://airbnb.io/lottie/#/android".toUri())
    }

    private fun showFragment(fragment: Fragment) {
        supportFragmentManager.beginTransaction()
            .replace(R.id.content, fragment)
            .commit()
    }
}

package com.airbnb.lottie.samples

import androidx.viewpager.widget.ViewPager

internal open class OnPageChangeListenerAdapter(
    private val onPageScrollStateChanged: ((state: Int) -> Unit)? = null,
    private val onPageScrolled:
    ((position: Int, positionOffset: Float, positionOffsetPixels: Int) -> Unit)? = null,
    private val onPageSelected: ((position: Int) -> Unit)? = null
) : ViewPager.OnPageChangeListener {
    override fun onPageScrollStateChanged(state: Int) =
        onPageScrollStateChanged?.invoke(state) ?: Unit

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) =
        onPageScrolled?.invoke(position, positionOffset, positionOffsetPixels) ?: Unit

    override fun onPageSelected(position: Int) = onPageSelected?.invoke(position) ?: Unit
}

package com.airbnb.lottie.samples

import android.widget.SeekBar

internal class OnSeekBarChangeListenerAdapter(
    private val onProgressChanged: ((seekBar: SeekBar, progress: Int, fromUser: Boolean) -> Unit)? = null,
    private val onStartTrackingTouch: ((seekBar: SeekBar) -> Unit)? = null,
    private val onStopTrackingTouch: ((seekBar: SeekBar) -> Unit)? = null
) : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) =
        onProgressChanged?.invoke(seekBar, progress, fromUser) ?: Unit

    override fun onStartTrackingTouch(seekBar: SeekBar) =
        onStartTrackingTouch?.invoke(seekBar) ?: Unit

    override fun onStopTrackingTouch(seekBar: SeekBar) =
        onStopTrackingTouch?.invoke(seekBar) ?: Unit
}

package com.airbnb.lottie.samples

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.samples.model.CompositionArgs

class PlayerActivity : AppCompatActivity(R.layout.player_activity) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (savedInstanceState == null) {
            val args = intent.getParcelableExtra(PlayerFragment.EXTRA_ANIMATION_ARGS) ?: CompositionArgs(fileUri = intent.data)
            supportFragmentManager.beginTransaction()
                .add(R.id.content, PlayerFragment.forAsset(args))
                .commit()
        }
    }

    companion object {
        fun intent(context: Context, args: CompositionArgs): Intent {
            return Intent(context, PlayerActivity::class.java).apply {
                putExtra(PlayerFragment.EXTRA_ANIMATION_ARGS, args)
            }
        }
    }
}

package com.airbnb.lottie.samples

import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.lottie.FontAssetDelegate
import com.airbnb.lottie.L
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.RenderMode
import com.airbnb.lottie.model.KeyPath
import com.airbnb.lottie.samples.databinding.PlayerFragmentBinding
import com.airbnb.lottie.samples.model.CompositionArgs
import com.airbnb.lottie.samples.utils.viewBinding
import com.airbnb.lottie.samples.views.BottomSheetItemView
import com.airbnb.lottie.samples.views.BottomSheetItemViewModel_
import com.airbnb.lottie.samples.views.ControlBarItemToggleView
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.github.mikephil.charting.components.LimitLine
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import kotlin.math.abs
import kotlin.math.roundToInt

class PlayerFragment : BaseMvRxFragment(R.layout.player_fragment) {
    private val binding: PlayerFragmentBinding by viewBinding()
    private val viewModel: PlayerViewModel by fragmentViewModel()

    private val transition = AutoTransition().apply { duration = 175 }
    private val renderTimesBehavior by lazy {
        BottomSheetBehavior.from(binding.bottomSheetRenderTimes.root).apply {
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
        }
    }
    private val warningsBehavior by lazy {
        BottomSheetBehavior.from(binding.bottomSheetWarnings.root).apply {
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
        }
    }
    private val keyPathsBehavior by lazy {
        BottomSheetBehavior.from(binding.bottomSheetKeyPaths.root).apply {
            peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
        }
    }
    private val lineDataSet by lazy {
        val entries = ArrayList<Entry>(101)
        repeat(101) { i -> entries.add(Entry(i.toFloat(), 0f)) }
        LineDataSet(entries, "Render Times").apply {
            mode = LineDataSet.Mode.CUBIC_BEZIER
            cubicIntensity = 0.3f
            setDrawCircles(false)
            lineWidth = 1.8f
            color = Color.BLACK
        }
    }

    private val animatorListener = AnimatorListenerAdapter(
        onStart = { binding.controlBarPlayerControls.playButton.isActivated = true },
        onEnd = {
            binding.controlBarPlayerControls.playButton.isActivated = false
            binding.animationView.performanceTracker?.logRenderTimes()
            updateRenderTimesPerLayer()
        },
        onCancel = {
            binding.controlBarPlayerControls.playButton.isActivated = false
        },
        onRepeat = {
            binding.animationView.performanceTracker?.logRenderTimes()
            updateRenderTimesPerLayer()
        }
    )

    @SuppressLint("SetTextI18n")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        (requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
        (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayShowTitleEnabled(false)
        setHasOptionsMenu(true)

        binding.controlBarPlayerControls.lottieVersionView.text = getString(R.string.lottie_version, BuildConfig.VERSION_NAME)

        binding.animationView.setFontAssetDelegate(object : FontAssetDelegate() {
            override fun fetchFont(fontFamily: String?): Typeface {
                return Typeface.DEFAULT
            }
        })

        val args = arguments?.getParcelable<CompositionArgs>(EXTRA_ANIMATION_ARGS)
            ?: throw IllegalArgumentException("No composition args specified")
        args.animationData?.bgColorInt?.let {
            binding.controlBarBackgroundColor.backgroundButton1.setBackgroundColor(it)
            binding.animationContainer.setBackgroundColor(it)
            invertColor(it)
        }

        args.animationDataV2?.bgColorInt?.let {
            binding.controlBarBackgroundColor.backgroundButton1.setBackgroundColor(it)
            binding.animationContainer.setBackgroundColor(it)
            invertColor(it)
        }

        binding.controlBarTrim.minFrameView.setOnClickListener { showMinFrameDialog() }
        binding.controlBarTrim.maxFrameView.setOnClickListener { showMaxFrameDialog() }
        viewModel.selectSubscribe(PlayerState::minFrame, PlayerState::maxFrame) { minFrame, maxFrame ->
            binding.animationView.setMinAndMaxFrame(minFrame, maxFrame)
            // I think this is a lint bug. It complains about int being <ErrorType>
            //noinspection StringFormatMatches
            binding.controlBarTrim.minFrameView.setText(resources.getString(R.string.min_frame, binding.animationView.minFrame.toInt()))
            //noinspection StringFormatMatches
            binding.controlBarTrim.maxFrameView.setText(resources.getString(R.string.max_frame, binding.animationView.maxFrame.toInt()))
        }

        viewModel.fetchAnimation(args)
        viewModel.asyncSubscribe(PlayerState::composition, onFail = {
            Snackbar.make(binding.coordinatorLayout, R.string.composition_load_error, Snackbar.LENGTH_LONG).show()
            Log.w(L.TAG, "Error loading composition.", it)
        }) {
            binding.loadingView.isVisible = false
            onCompositionLoaded(it)
        }

        binding.controlBar.borderToggle.setOnClickListener { viewModel.toggleBorderVisible() }
        viewModel.selectSubscribe(PlayerState::borderVisible) {
            binding.controlBar.borderToggle.isActivated = it
            binding.controlBar.borderToggle.setImageResource(
                if (it) R.drawable.ic_border_on
                else R.drawable.ic_border_off
            )
            binding.animationView.setBackgroundResource(if (it) R.drawable.outline else 0)
        }

        binding.controlBar.hardwareAccelerationToggle.setOnClickListener {
            val renderMode = if (binding.animationView.renderMode == RenderMode.HARDWARE) {
                RenderMode.SOFTWARE
            } else {
                RenderMode.HARDWARE
            }
            binding.animationView.renderMode = renderMode
            binding.controlBar.hardwareAccelerationToggle.isActivated = binding.animationView.renderMode == RenderMode.HARDWARE
        }

        binding.controlBar.enableApplyingOpacityToLayers.setOnClickListener {
            val isApplyingOpacityToLayersEnabled = !binding.controlBar.enableApplyingOpacityToLayers.isActivated
            binding.animationView.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled)
            binding.controlBar.enableApplyingOpacityToLayers.isActivated = isApplyingOpacityToLayersEnabled
        }

        viewModel.selectSubscribe(PlayerState::controlsVisible) { binding.controlBarPlayerControls.controlsContainer.animateVisible(it) }

        viewModel.selectSubscribe(PlayerState::controlBarVisible) { binding.controlBar.root.animateVisible(it) }

        binding.controlBar.renderGraphToggle.setOnClickListener { viewModel.toggleRenderGraphVisible() }
        viewModel.selectSubscribe(PlayerState::renderGraphVisible) {
            binding.controlBar.renderGraphToggle.isActivated = it
            binding.controlBarPlayerControls.renderTimesGraphContainer.animateVisible(it)
            binding.controlBarPlayerControls.renderTimesPerLayerButton.animateVisible(it)
            binding.controlBarPlayerControls.lottieVersionView.animateVisible(!it)
        }

        binding.controlBar.masksAndMattesToggle.setOnClickListener { viewModel.toggleOutlineMasksAndMattes() }
        viewModel.selectSubscribe(PlayerState::outlineMasksAndMattes) {
            binding.controlBar.masksAndMattesToggle.isActivated = it
            binding.animationView.setOutlineMasksAndMattes(it)
        }

        binding.controlBar.backgroundColorToggle.setOnClickListener { viewModel.toggleBackgroundColorVisible() }
        binding.controlBarBackgroundColor.closeBackgroundColorButton.setOnClickListener { viewModel.setBackgroundColorVisible(false) }
        viewModel.selectSubscribe(PlayerState::backgroundColorVisible) {
            binding.controlBar.backgroundColorToggle.isActivated = it
            binding.controlBarBackgroundColor.backgroundColorContainer.animateVisible(it)
        }

        binding.controlBar.trimToggle.setOnClickListener { viewModel.toggleTrimVisible() }
        binding.controlBarTrim.closeTrimButton.setOnClickListener { viewModel.setTrimVisible(false) }
        viewModel.selectSubscribe(PlayerState::trimVisible) {
            binding.controlBar.trimToggle.isActivated = it
            binding.controlBarTrim.trimContainer.animateVisible(it)
        }

        binding.controlBar.mergePathsToggle.setOnClickListener { viewModel.toggleMergePaths() }
        viewModel.selectSubscribe(PlayerState::useMergePaths) {
            binding.animationView.enableMergePathsForKitKatAndAbove(it)
            binding.controlBar.mergePathsToggle.isActivated = it
        }

        binding.controlBar.speedToggle.setOnClickListener { viewModel.toggleSpeedVisible() }
        binding.controlBarSpeed.closeSpeedButton.setOnClickListener { viewModel.setSpeedVisible(false) }
        viewModel.selectSubscribe(PlayerState::speedVisible) {
            binding.controlBar.speedToggle.isActivated = it
            binding.controlBarSpeed.speedContainer.isVisible = it
        }
        viewModel.selectSubscribe(PlayerState::speed) {
            binding.animationView.speed = it
            binding.controlBarSpeed.speedButtonsContainer
                .children
                .filterIsInstance<ControlBarItemToggleView>()
                .forEach { toggleView ->
                    toggleView.isActivated = toggleView.getText().replace("x", "").toFloat() == binding.animationView.speed
                }
        }
        binding.controlBarSpeed.speedButtonsContainer
            .children
            .filterIsInstance(ControlBarItemToggleView::class.java)
            .forEach { child ->
                child.setOnClickListener {
                    val speed = (it as ControlBarItemToggleView)
                        .getText()
                        .replace("x", "")
                        .toFloat()
                    viewModel.setSpeed(speed)
                }
            }

        binding.controlBarPlayerControls.loopButton.setOnClickListener { viewModel.toggleLoop() }
        viewModel.selectSubscribe(PlayerState::repeatCount) {
            binding.animationView.repeatCount = it
            binding.controlBarPlayerControls.loopButton.isActivated = binding.animationView.repeatCount == ValueAnimator.INFINITE
        }

        binding.controlBarPlayerControls.playButton.isActivated = binding.animationView.isAnimating

        binding.controlBarPlayerControls.seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
            onProgressChanged = { _, progress, _ ->
                if (binding.controlBarPlayerControls.seekBar.isPressed && progress in 1..4) {
                    binding.controlBarPlayerControls.seekBar.progress = 0
                    return@OnSeekBarChangeListenerAdapter
                }
                if (binding.animationView.isAnimating) return@OnSeekBarChangeListenerAdapter
                binding.animationView.progress = progress / binding.controlBarPlayerControls.seekBar.max.toFloat()
            }
        ))

        binding.animationView.addAnimatorUpdateListener {
            binding.controlBarPlayerControls.currentFrameView.text = updateFramesAndDurationLabel(binding.animationView)

            if (binding.controlBarPlayerControls.seekBar.isPressed) return@addAnimatorUpdateListener
            binding.controlBarPlayerControls.seekBar.progress =
                ((it.animatedValue as Float) * binding.controlBarPlayerControls.seekBar.max).roundToInt()
        }
        binding.animationView.addAnimatorListener(animatorListener)
        binding.controlBarPlayerControls.playButton.setOnClickListener {
            if (binding.animationView.isAnimating) binding.animationView.pauseAnimation() else binding.animationView.resumeAnimation()
            binding.controlBarPlayerControls.playButton.isActivated = binding.animationView.isAnimating
            postInvalidate()
        }

        binding.animationView.setOnClickListener {
            // Click the animation view to re-render it for debugging purposes.
            binding.animationView.invalidate()
        }

        arrayOf(
            binding.controlBarBackgroundColor.backgroundButton1,
            binding.controlBarBackgroundColor.backgroundButton2,
            binding.controlBarBackgroundColor.backgroundButton3,
            binding.controlBarBackgroundColor.backgroundButton4,
            binding.controlBarBackgroundColor.backgroundButton5,
            binding.controlBarBackgroundColor.backgroundButton6
        ).forEach { bb ->
            bb.setOnClickListener {
                binding.animationContainer.setBackgroundColor(bb.getColor())
                invertColor(bb.getColor())
            }
        }

        binding.controlBarPlayerControls.renderTimesGraph.apply {
            setTouchEnabled(false)
            axisRight.isEnabled = false
            xAxis.isEnabled = false
            legend.isEnabled = false
            description = null
            data = LineData(lineDataSet)
            axisLeft.setDrawGridLines(false)
            axisLeft.labelCount = 4
            val ll1 = LimitLine(16f, "60fps")
            ll1.lineColor = Color.RED
            ll1.lineWidth = 1.2f
            ll1.textColor = Color.BLACK
            ll1.textSize = 8f
            axisLeft.addLimitLine(ll1)

            val ll2 = LimitLine(32f, "30fps")
            ll2.lineColor = Color.RED
            ll2.lineWidth = 1.2f
            ll2.textColor = Color.BLACK
            ll2.textSize = 8f
            axisLeft.addLimitLine(ll2)
        }

        binding.controlBarPlayerControls.renderTimesPerLayerButton.setOnClickListener {
            updateRenderTimesPerLayer()
            renderTimesBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
        }

        binding.bottomSheetRenderTimes.closeRenderTimesBottomSheetButton.setOnClickListener {
            renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN
        }
        renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN

        binding.controlBar.warningsButton.setOnClickListener {
            withState(viewModel) { state ->
                if (state.composition()?.warnings?.isEmpty() != true) {
                    warningsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED

                }
            }
        }

        binding.bottomSheetWarnings.closeWarningsBottomSheetButton.setOnClickListener {
            warningsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
        }
        warningsBehavior.state = BottomSheetBehavior.STATE_HIDDEN

        binding.controlBar.keyPathsToggle.setOnClickListener {
            keyPathsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
        }

        binding.bottomSheetKeyPaths.closeKeyPathsBottomSheetButton.setOnClickListener {
            keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
        }
        keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
    }

    private fun showMinFrameDialog() {
        val minFrameView = EditText(context)
        minFrameView.setText(binding.animationView.minFrame.toInt().toString())
        AlertDialog.Builder(context)
            .setTitle(R.string.min_frame_dialog)
            .setView(minFrameView)
            .setPositiveButton("Load") { _, _ ->
                viewModel.setMinFrame(minFrameView.text.toString().toIntOrNull() ?: 0)
            }
            .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
            .show()
    }

    private fun showMaxFrameDialog() {
        val maxFrameView = EditText(context)
        maxFrameView.setText(binding.animationView.maxFrame.toInt().toString())
        AlertDialog.Builder(context)
            .setTitle(R.string.max_frame_dialog)
            .setView(maxFrameView)
            .setPositiveButton("Load") { _, _ ->
                viewModel.setMaxFrame(maxFrameView.text.toString().toIntOrNull() ?: 0)
            }
            .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
            .show()
    }

    private fun View.animateVisible(visible: Boolean) {
        beginDelayedTransition()
        isVisible = visible
    }

    private fun invertColor(color: Int) {
        val isDarkBg = color.isDark()
        binding.animationView.isActivated = isDarkBg
        binding.toolbar.isActivated = isDarkBg
    }

    private fun Int.isDark(): Boolean {
        val y = (299 * Color.red(this) + 587 * Color.green(this) + 114 * Color.blue(this)) / 1000
        return y < 128
    }

    override fun onDestroyView() {
        binding.animationView.removeAnimatorListener(animatorListener)
        super.onDestroyView()
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.fragment_player, menu)
        super.onCreateOptionsMenu(menu, inflater)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.isCheckable) item.isChecked = !item.isChecked
        when (item.itemId) {
            android.R.id.home -> requireActivity().finish()
            R.id.info -> Unit
            R.id.visibility -> {
                viewModel.setDistractionFree(item.isChecked)
                val menuIcon = if (item.isChecked) R.drawable.ic_eye_teal else R.drawable.ic_eye_selector
                item.icon = ContextCompat.getDrawable(requireContext(), menuIcon)
            }
        }
        return true
    }

    private fun onCompositionLoaded(composition: LottieComposition?) {
        composition ?: return

        // If the composition is missing any images, return the original image or null.
        if (composition.images.any { (_, asset) -> !asset.hasBitmap() }) {
            binding.animationView.setImageAssetDelegate { it.bitmap }
        }

        binding.animationView.setComposition(composition)
        binding.controlBar.hardwareAccelerationToggle.isActivated = binding.animationView.renderMode == RenderMode.HARDWARE
        binding.animationView.setPerformanceTrackingEnabled(true)
        var renderTimeGraphRange = 4f
        binding.animationView.performanceTracker?.addFrameListener { ms ->
            if (lifecycle.currentState != Lifecycle.State.RESUMED) return@addFrameListener
            lineDataSet.getEntryForIndex((binding.animationView.progress * 100).toInt()).y = ms
            renderTimeGraphRange = renderTimeGraphRange.coerceAtLeast(ms * 1.2f)
            binding.controlBarPlayerControls.renderTimesGraph.setVisibleYRange(0f, renderTimeGraphRange, YAxis.AxisDependency.LEFT)
            binding.controlBarPlayerControls.renderTimesGraph.invalidate()
        }

        binding.bottomSheetKeyPaths.keyPathsRecyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
            override fun buildModels(controller: EpoxyController) {
                binding.animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
                    BottomSheetItemViewModel_()
                        .id(index)
                        .text(keyPath.keysToString())
                        .addTo(controller)
                }
            }
        })

        updateWarnings()
    }

    override fun invalidate() {
    }

    private fun updateRenderTimesPerLayer() {
        binding.bottomSheetRenderTimes.renderTimesContainer.removeAllViews()
        binding.animationView.performanceTracker?.sortedRenderTimes?.forEach {
            val view = BottomSheetItemView(requireContext()).apply {
                set(
                    it.first!!.replace("__container", "Total"),
                    "%.2f ms".format(it.second!!)
                )
            }
            binding.bottomSheetRenderTimes.renderTimesContainer.addView(view)
        }
        binding.animationView.performanceTracker?.clearRenderTimes()
    }

    private fun updateWarnings() = withState(viewModel) { state ->
        // Force warning to update
        binding.bottomSheetWarnings.warningsContainer.removeAllViews()

        val warnings = state.composition()?.warnings ?: emptySet<String>()
        if (!warnings.isEmpty() && warnings.size == binding.bottomSheetWarnings.warningsContainer.childCount) return@withState

        binding.bottomSheetWarnings.warningsContainer.removeAllViews()
        warnings.forEach {
            val view = BottomSheetItemView(requireContext()).apply {
                set(it)
            }
            binding.bottomSheetWarnings.warningsContainer.addView(view)
        }

        val size = warnings.size
        binding.controlBar.warningsButton.setText(resources.getQuantityString(R.plurals.warnings, size, size))
        binding.controlBar.warningsButton.setImageResource(
            if (warnings.isEmpty()) R.drawable.ic_sentiment_satisfied
            else R.drawable.ic_sentiment_dissatisfied
        )
    }

    private fun beginDelayedTransition() = TransitionManager.beginDelayedTransition(binding.container, transition)

    companion object {
        const val EXTRA_ANIMATION_ARGS = "animation_args"

        fun forAsset(args: CompositionArgs): Fragment {
            return PlayerFragment().apply {
                arguments = Bundle().apply {
                    putParcelable(EXTRA_ANIMATION_ARGS, args)
                }
            }
        }
    }

    private fun updateFramesAndDurationLabel(animation: LottieAnimationView): String {
        val currentFrame = animation.frame.toString()
        val totalFrames = ("%.0f").format(animation.maxFrame)

        val animationSpeed: Float = abs(animation.speed)

        val totalTime = ((animation.duration / animationSpeed) / 1000.0)
        val totalTimeFormatted = ("%.1f").format(totalTime)

        val progress = (totalTime / 100.0) * ((animation.progress * 100.0).roundToInt())
        val progressFormatted = ("%.1f").format(progress)

        return "$currentFrame/$totalFrames\n$progressFormatted/$totalTimeFormatted"
    }
}

package com.airbnb.lottie.samples

import android.animation.ValueAnimator
import android.app.Application
import android.net.Uri
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieTask
import com.airbnb.lottie.samples.model.CompositionArgs
import com.airbnb.lottie.samples.utils.MvRxViewModel
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import java.io.FileInputStream
import kotlin.math.max
import kotlin.math.min

data class PlayerState(
    val composition: Async<LottieComposition> = Uninitialized,
    val controlsVisible: Boolean = true,
    val controlBarVisible: Boolean = true,
    val renderGraphVisible: Boolean = false,
    val outlineMasksAndMattes: Boolean = false,
    val borderVisible: Boolean = false,
    val backgroundColorVisible: Boolean = false,
    val speedVisible: Boolean = false,
    val trimVisible: Boolean = false,
    val useMergePaths: Boolean = false,
    val minFrame: Int = 0,
    val maxFrame: Int = 0,
    val speed: Float = 1f,
    val repeatCount: Int = ValueAnimator.INFINITE
) : MvRxState

class PlayerViewModel(
    initialState: PlayerState,
    private val application: Application
) : MvRxViewModel<PlayerState>(initialState) {

    fun fetchAnimation(args: CompositionArgs) {
        val url = args.url ?: args.animationDataV2?.file ?: args.animationData?.lottieLink

        when {
            url != null -> LottieCompositionFactory.fromUrl(application, url, null)
            args.fileUri != null -> taskForUri(args.fileUri)
            args.asset != null -> LottieCompositionFactory.fromAsset(application, args.asset, null)
            else -> error("Don't know how to fetch animation for $args")
        }
            .addListener {
                setState {
                    copy(composition = Success(it), minFrame = it.startFrame.toInt(), maxFrame = it.endFrame.toInt())
                }
            }
            .addFailureListener { setState { copy(composition = Fail(it)) } }
    }

    private fun taskForUri(uri: Uri): LottieTask<LottieComposition> {
        val fis = when (uri.scheme) {
            "file" -> FileInputStream(uri.path ?: error("File has no path!"))
            "content" -> application.contentResolver.openInputStream(uri)
            else -> error("Unknown scheme ${uri.scheme}")
        }

        return LottieCompositionFactory.fromJsonInputStream(fis, null)
    }

    fun toggleRenderGraphVisible() = setState { copy(renderGraphVisible = !renderGraphVisible) }

    fun toggleOutlineMasksAndMattes() = setState { copy(outlineMasksAndMattes = !outlineMasksAndMattes) }

    fun toggleBorderVisible() = setState { copy(borderVisible = !borderVisible) }

    fun toggleBackgroundColorVisible() = setState { copy(backgroundColorVisible = !backgroundColorVisible) }

    fun setBackgroundColorVisible(visible: Boolean) = setState { copy(backgroundColorVisible = visible) }

    fun toggleSpeedVisible() = setState { copy(speedVisible = !speedVisible) }

    fun setSpeedVisible(visible: Boolean) = setState { copy(speedVisible = visible) }

    fun toggleTrimVisible() = setState { copy(trimVisible = !trimVisible) }

    fun setTrimVisible(visible: Boolean) = setState { copy(trimVisible = visible) }

    fun toggleMergePaths() = setState { copy(useMergePaths = !useMergePaths) }

    fun setMinFrame(minFrame: Int) = setState {
        copy(minFrame = max(minFrame, composition()?.startFrame?.toInt() ?: 0))
    }

    fun setMaxFrame(maxFrame: Int) = setState {
        copy(maxFrame = min(maxFrame, composition()?.endFrame?.toInt() ?: 0))
    }

    fun setSpeed(speed: Float) = setState { copy(speed = speed) }

    fun toggleLoop() = setState { copy(repeatCount = if (repeatCount == ValueAnimator.INFINITE) 0 else ValueAnimator.INFINITE) }

    fun setDistractionFree(distractionFree: Boolean) = setState {
        copy(
            controlsVisible = !distractionFree,
            controlBarVisible = !distractionFree,
            renderGraphVisible = false,
            borderVisible = false,
            backgroundColorVisible = false,
            speedVisible = false,
            trimVisible = false
        )
    }

    companion object : MvRxViewModelFactory<PlayerViewModel, PlayerState> {
        override fun create(viewModelContext: ViewModelContext, state: PlayerState): PlayerViewModel {
            return PlayerViewModel(state, viewModelContext.app())
        }
    }
}

package com.airbnb.lottie.samples

import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Toast
import com.airbnb.epoxy.EpoxyController
import com.airbnb.lottie.samples.model.CompositionArgs
import com.airbnb.lottie.samples.utils.BaseEpoxyFragment
import com.airbnb.lottie.samples.utils.hasPermission
import com.airbnb.lottie.samples.views.marquee
import com.google.android.material.snackbar.Snackbar

private const val RC_FILE = 1000
private const val RC_CAMERA_PERMISSION = 1001

class PreviewFragment : BaseEpoxyFragment() {

    override fun EpoxyController.buildModels() {
        marquee {
            id("marquee")
            title(R.string.preview_title)
        }

        previewItemView {
            id("qr")
            title(R.string.preview_qr)
            icon(R.drawable.ic_qr_scan)
            clickListener { _ ->
                if (requireContext().hasPermission(Manifest.permission.CAMERA)) {
                    startActivity(QRScanActivity.intent(requireContext()))
                } else {
                    @Suppress("DEPRECATION")
                    requestPermissions(arrayOf(Manifest.permission.CAMERA), RC_CAMERA_PERMISSION)
                }
            }
        }

        previewItemView {
            id("file")
            title(R.string.preview_file)
            icon(R.drawable.ic_file)
            clickListener { _ ->
                try {
                    val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
                        type = "*/*"
                        addCategory(Intent.CATEGORY_OPENABLE)
                    }
                    @Suppress("DEPRECATION")
                    startActivityForResult(Intent.createChooser(intent, "Select a JSON file"), RC_FILE)
                } catch (ex: ActivityNotFoundException) {
                    // Potentially direct the user to the Market with a Dialog
                    Toast.makeText(context, "Please install a File Manager.", Toast.LENGTH_SHORT).show()
                }
            }
        }

        previewItemView {
            id("url")
            title(R.string.preview_url)
            icon(R.drawable.ic_network)
            clickListener { _ ->
                val urlOrJsonView = EditText(context)
                AlertDialog.Builder(context)
                    .setTitle(R.string.preview_url)
                    .setView(urlOrJsonView)
                    .setPositiveButton(R.string.preview_load) { _, _ ->
                        val args = CompositionArgs(url = urlOrJsonView.text.toString())
                        startActivity(PlayerActivity.intent(requireContext(), args))
                    }
                    .setNegativeButton(R.string.preview_cancel) { dialog, _ -> dialog.dismiss() }
                    .show()
            }
        }

        previewItemView {
            id("assets")
            title(R.string.preview_assets)
            icon(R.drawable.ic_storage)
            clickListener { _ ->
                val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.select_dialog_item)
                requireContext().assets.list("")?.asSequence()
                    ?.filter { it.endsWith(".json") || it.endsWith(".zip") }
                    ?.forEach { adapter.add(it) }
                AlertDialog.Builder(context)
                    .setAdapter(adapter) { _, which ->
                        val args = CompositionArgs(asset = adapter.getItem(which))
                        startActivity(PlayerActivity.intent(requireContext(), args))
                    }
                    .show()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode != Activity.RESULT_OK) return
        when (requestCode) {
            RC_FILE -> startActivity(PlayerActivity.intent(requireContext(), CompositionArgs(fileUri = data?.data)))
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        when (requestCode) {
            RC_CAMERA_PERMISSION -> {
                if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
                    startActivity(QRScanActivity.intent(requireContext()))
                } else {
                    Snackbar.make(binding.root, R.string.qr_permission_denied, Snackbar.LENGTH_LONG).show()
                }
            }
        }
    }
}

package com.airbnb.lottie.samples

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.TextProp
import com.airbnb.lottie.samples.databinding.ListItemPreviewBinding
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class PreviewItemView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val binding: ListItemPreviewBinding by viewBinding()

    init {
        orientation = VERTICAL
    }

    @TextProp
    fun setTitle(title: CharSequence) {
        binding.titleView.text = title
    }

    @ModelProp
    fun setIcon(@DrawableRes icon: Int) {
        binding.iconView.setImageResource(icon)
    }

    @CallbackProp
    fun setClickListener(clickListener: OnClickListener?) {
        binding.container.setOnClickListener(clickListener)
    }
}

package com.airbnb.lottie.samples

import android.content.Context
import android.content.Intent
import android.graphics.PointF
import android.os.Bundle
import android.os.Vibrator
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.samples.databinding.QrscanActivityBinding
import com.airbnb.lottie.samples.model.CompositionArgs
import com.airbnb.lottie.samples.utils.vibrateCompat
import com.airbnb.lottie.samples.utils.viewBinding
import com.dlazaro66.qrcodereaderview.QRCodeReaderView

class QRScanActivity : AppCompatActivity(), QRCodeReaderView.OnQRCodeReadListener {
    private val binding: QrscanActivityBinding by viewBinding()
    @Suppress("DEPRECATION")
    private val vibrator by lazy { getSystemService(Context.VIBRATOR_SERVICE) as Vibrator }

    // Sometimes the qr code is read twice in rapid succession. This prevents it from being read
    // multiple times.
    private var hasReadQrCode = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.qrView.setQRDecodingEnabled(true)
        binding.qrView.setAutofocusInterval(2000L)
        binding.qrView.setBackCamera()
        binding.qrView.setOnQRCodeReadListener(this)
        binding.qrView.setOnClickListener { binding.qrView.forceAutoFocus() }
    }

    override fun onResume() {
        super.onResume()
        binding.qrView.startCamera()
        hasReadQrCode = false
    }

    override fun onPause() {
        super.onPause()
        binding.qrView.stopCamera()
    }

    override fun onQRCodeRead(url: String, pointFS: Array<PointF>) {
        if (hasReadQrCode) return
        hasReadQrCode = true
        vibrator.vibrateCompat(100)
        finish()
        startActivity(PlayerActivity.intent(this, CompositionArgs(url = url)))
    }

    companion object {
        fun intent(context: Context) = Intent(context, QRScanActivity::class.java)
    }
}

package com.airbnb.lottie.samples

import android.content.Intent
import com.airbnb.epoxy.EpoxyController
import com.airbnb.lottie.samples.api.LottiefilesApi
import com.airbnb.lottie.samples.model.AnimationResponseV2
import com.airbnb.lottie.samples.model.CompositionArgs
import com.airbnb.lottie.samples.model.ShowcaseItem
import com.airbnb.lottie.samples.utils.BaseEpoxyFragment
import com.airbnb.lottie.samples.utils.MvRxViewModel
import com.airbnb.lottie.samples.views.animationItemView
import com.airbnb.lottie.samples.views.loadingView
import com.airbnb.lottie.samples.views.marquee
import com.airbnb.lottie.samples.views.showcaseCarousel
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState

data class ShowcaseState(val response: Async<AnimationResponseV2> = Uninitialized) : MvRxState

class ShowcaseViewModel(initialState: ShowcaseState, api: LottiefilesApi) : MvRxViewModel<ShowcaseState>(initialState) {
    init {
        suspend {
            api.getCollection()
        }.execute { copy(response = it) }
    }

    companion object : MvRxViewModelFactory<ShowcaseViewModel, ShowcaseState> {
        override fun create(viewModelContext: ViewModelContext, state: ShowcaseState): ShowcaseViewModel {
            val service = viewModelContext.app<LottieApplication>().lottiefilesService
            return ShowcaseViewModel(state, service)
        }
    }
}

class ShowcaseFragment : BaseEpoxyFragment() {

    private val showcaseItems = listOf(
        ShowcaseItem(R.drawable.showcase_preview_lottie, R.string.showcase_item_dynamic_properties) {
            it.startActivity(Intent(it, DynamicActivity::class.java))
        },
        ShowcaseItem(R.drawable.gilbert_animated, R.string.showcase_item_animated_text) {
            it.startActivity(Intent(it, TypographyDemoActivity::class.java))
        },
        ShowcaseItem(R.drawable.gilbert_animated, R.string.showcase_item_dynamic_text) {
            it.startActivity(Intent(it, DynamicTextActivity::class.java))
        },
        ShowcaseItem(R.drawable.showcase_preview_lottie, R.string.showcase_item_bullseye) {
            it.startActivity(Intent(it, BullseyeActivity::class.java))
        },
        ShowcaseItem(R.drawable.showcase_preview_lottie, R.string.showcase_item_recycler_view) {
            it.startActivity(Intent(it, WishListActivity::class.java))
        }
    )
    private val viewModel: ShowcaseViewModel by fragmentViewModel()

    override fun EpoxyController.buildModels() = withState(viewModel) { state ->
        marquee {
            id("showcase")
            title("Showcase")
        }
        showcaseCarousel {
            id("carousel")
            showcaseItems(showcaseItems)
        }

        val collectionItems = state.response()?.data

        if (collectionItems == null) {
            loadingView {
                id("loading")
            }
        } else {
            collectionItems.forEach {
                val activityContext = requireActivity()
                animationItemView {
                    id(it.id)
                    title(it.title)
                    if (it.preview != null) previewUrl("https://assets9.lottiefiles.com/${it.preview}")
                    previewBackgroundColor(it.bgColorInt)
                    onClickListener { _ -> activityContext.startActivity(PlayerActivity.intent(activityContext, CompositionArgs(animationDataV2 = it))) }
                }
            }
        }
    }
}

package com.airbnb.lottie.samples

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import com.airbnb.lottie.samples.databinding.SimpleAnimationActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding

/**
 * Useful for performance debugging.
 * adb shell am start -n com.airbnb.lottie/.samples.SimpleAnimationActivity --es animation LottieLogo1.json --activity-clear-top
 */
class SimpleAnimationActivity : AppCompatActivity() {
    private val binding: SimpleAnimationActivityBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var composition: LottieComposition? = null
        binding.parse.setOnClickListener {
            val assetName = intent.extras?.getString("animation") ?: ""
            val start = System.currentTimeMillis()
            composition = LottieCompositionFactory.fromAssetSync(this, assetName, null).value
                ?: throw IllegalArgumentException("Invalid composition $assetName")
            Toast.makeText(this@SimpleAnimationActivity, "Done ${System.currentTimeMillis() - start}", Toast.LENGTH_SHORT).show()
        }

        binding.setComposition.setOnClickListener {
            val start = System.currentTimeMillis()
            val drawable = LottieDrawable()
            drawable.composition = composition
            Toast.makeText(this@SimpleAnimationActivity, "Done ${System.currentTimeMillis() - start}", Toast.LENGTH_SHORT).show()
        }

        binding.play.setOnClickListener {
            composition?.let { binding.animationView.setComposition(it) }
            binding.animationView.playAnimation()
        }
    }
}

package com.airbnb.lottie.samples

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.customview.widget.ViewDragHelper

class TrimView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val leftAnchor by lazy {
        val iv = ImageView(context)
        iv.setImageResource(R.drawable.ic_trim)
        iv
    }
    private val rightAnchor by lazy {
        val iv = ImageView(context)
        iv.setImageResource(R.drawable.ic_trim)
        iv
    }
    private lateinit var callback: (Float, Float) -> Unit

    private val dragHelper = ViewDragHelper.create(this, object : ViewDragHelper.Callback() {
        override fun tryCaptureView(child: View, pointerId: Int) = true

        override fun getViewHorizontalDragRange(child: View) = width

        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
            return if (child == leftAnchor) {
                maxOf(minOf(left, rightAnchor.left - leftAnchor.width), 0)
            } else {
                minOf(maxOf(leftAnchor.right, left), width - rightAnchor.width)
            }
        }

        override fun onViewPositionChanged(view: View, left: Int, top: Int, dx: Int, dy: Int) {
            val startProgress = leftAnchor.left / width.toFloat()
            val endProgress = rightAnchor.right / width.toFloat()
            callback(startProgress, endProgress)
        }
    })

    init {
        val leftLp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        leftLp.gravity = Gravity.START
        leftAnchor.layoutParams = leftLp
        addView(leftAnchor)
        val rightLp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        rightLp.gravity = Gravity.END
        rightAnchor.layoutParams = rightLp
        addView(rightAnchor)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (dragHelper.shouldInterceptTouchEvent(ev)) {
            return true
        }
        return super.onInterceptTouchEvent(ev)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        dragHelper.processTouchEvent(event)
        return true
    }

    fun setCallback(callback: (Float, Float) -> Unit) {
        this.callback = callback
    }
}

package com.airbnb.lottie.samples

import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.lottie.samples.databinding.TypographyDemoActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding

class TypographyDemoActivity : AppCompatActivity() {
    private val binding: TypographyDemoActivityBinding by viewBinding()

    private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
        binding.scrollView.fullScroll(View.FOCUS_DOWN)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.fontView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
    }

    override fun onDestroy() {
        binding.fontView.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
        super.onDestroy()
    }
}

package com.airbnb.lottie.samples

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.lottie.samples.databinding.ListActivityBinding
import com.airbnb.lottie.samples.utils.viewBinding
import com.airbnb.lottie.samples.views.listingCard
import com.airbnb.lottie.samples.views.marquee

class WishListActivity : AppCompatActivity() {
    private val binding: ListActivityBinding by viewBinding()

    private val wishListedItems = mutableSetOf<Int>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.recyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
            override fun buildModels(controller: EpoxyController) {
                controller.buildModels()
            }
        })
    }

    private fun EpoxyController.buildModels() {
        marquee {
            id("marquee")
            title("List")
            subtitle("Loading the same animation many times in a list")
        }

        repeat(100) { index ->
            listingCard {
                id(index)
                isWishListed(wishListedItems.contains(index))
                onToggled { isWishListed ->
                    if (isWishListed) wishListedItems.add(index)
                    else wishListedItems.remove(index)
                    binding.recyclerView.requestModelBuild()
                }
            }
        }
    }
}

package com.airbnb.lottie.issues.compose

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition

class ComposeIssueReproActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Content()
        }
    }

    @Composable
    fun Content() {
        val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
        val progress by animateLottieCompositionAsState(composition)
        LottieAnimation(composition, progress)
    }
}

package com.airbnb.lottie.sample.compose

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
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.CachingExamplesPage
import com.airbnb.lottie.sample.compose.examples.ContentScaleExamplesPage
import com.airbnb.lottie.sample.compose.examples.DynamicPropertiesExamplesPage
import com.airbnb.lottie.sample.compose.examples.ExamplesPage
import com.airbnb.lottie.sample.compose.examples.ImagesExamplesPage
import com.airbnb.lottie.sample.compose.examples.NetworkExamplesPage
import com.airbnb.lottie.sample.compose.examples.TextExamplesPage
import com.airbnb.lottie.sample.compose.examples.TransitionsExamplesPage
import com.airbnb.lottie.sample.compose.examples.ViewPagerExamplePage
import com.airbnb.lottie.sample.compose.lottiefiles.LottieFilesPage
import com.airbnb.lottie.sample.compose.player.PlayerPage
import com.airbnb.lottie.sample.compose.preview.PreviewPage
import com.airbnb.lottie.sample.compose.showcase.ShowcasePage
import com.airbnb.lottie.sample.compose.ui.LottieTheme
import com.airbnb.lottie.sample.compose.ui.Teal
import com.airbnb.lottie.sample.compose.ui.toColorSafe
import com.airbnb.lottie.sample.compose.utils.getBase64String

class ComposeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LottieScaffold()
        }
    }

    @Composable
    private fun LottieScaffold() {
        val navController = rememberNavController()

        LottieTheme {
            Scaffold(
                bottomBar = {
                    BottomNavigation(
                        backgroundColor = Color(0xFFF7F7F7),
                        elevation = 8.dp,
                        contentColor = Teal,
                    ) {
                        val navBackStackEntry by navController.currentBackStackEntryAsState()
                        val currentRoute = navBackStackEntry?.destination?.route

                        BottomNavItemData.values().forEach { item ->
                            BottomNavigationItem(
                                icon = {
                                    Icon(
                                        painter = painterResource(item.iconRes),
                                        contentDescription = null
                                    )
                                },
                                label = { Text(stringResource(item.labelRes)) },
                                selected = currentRoute == item.route.route,
                                onClick = {
                                    if (currentRoute != item.route.route) {
                                        navController.navigate(item.route.route)
                                    }
                                },
                            )
                        }
                    }
                }
            ) { innerPadding ->
                Box(
                    modifier = Modifier.padding(innerPadding)
                ) {
                    NavHost(navController, startDestination = Route.Showcase.route) {
                        composable(Route.Showcase.route) { ShowcasePage(navController) }
                        composable(Route.Preview.route) { PreviewPage(navController) }
                        composable(Route.LottieFiles.route) { LottieFilesPage(navController) }
                        composable(Route.Examples.route) { ExamplesPage(navController) }
                        composable(Route.BasicUsageExamples.route) { BasicUsageExamplesPage() }
                        composable(Route.AnimatableUsageExamples.route) { AnimatableExamplesPage() }
                        composable(Route.TransitionsExamples.route) { TransitionsExamplesPage() }
                        composable(Route.ViewPagerExample.route) { ViewPagerExamplePage() }
                        composable(Route.NetworkExamples.route) { NetworkExamplesPage() }
                        composable(Route.DynamicPropertiesExamples.route) { DynamicPropertiesExamplesPage() }
                        composable(Route.ImagesExamples.route) { ImagesExamplesPage() }
                        composable(Route.TextExamples.route) { TextExamplesPage() }
                        composable(Route.ContentScaleExamples.route) { ContentScaleExamplesPage() }
                        composable(Route.CachingExamples.route) { CachingExamplesPage() }
                        composable(
                            Route.Player.fullRoute,
                            arguments = Route.Player.args
                        ) { entry ->
                            val arguments = entry.arguments ?: error("No arguments provided to ${Route.Player}")
                            val spec = when {
                                arguments.getString("url") != null -> LottieCompositionSpec.Url(arguments.getBase64String("url"))
                                arguments.getString("file") != null -> LottieCompositionSpec.File(arguments.getBase64String("file"))
                                arguments.getString("asset") != null -> LottieCompositionSpec.Asset(arguments.getBase64String("asset"))
                                else -> error("You must specify a url, file, or asset")
                            }
                            val backgroundColor = when (arguments.getString("backgroundColor") != null) {
                                true -> arguments.getBase64String("backgroundColor").toColorSafe()
                                else -> null
                            }
                            PlayerPage(spec, backgroundColor)
                        }
                    }
                }
            }
        }
    }

    private enum class BottomNavItemData(val route: Route, @DrawableRes val iconRes: Int, @StringRes val labelRes: Int) {
        Showcase(Route.Showcase, R.drawable.ic_showcase, R.string.bottom_tab_showcase),
        Preview(Route.Preview, R.drawable.ic_device, R.string.bottom_tab_preview),
        LottieFiles(Route.LottieFiles, R.drawable.ic_lottie_files, R.string.bottom_tab_lottie_files),
        Docs(Route.Examples, R.drawable.ic_examples, R.string.bottom_tab_examples),
    }
}

package com.airbnb.lottie.sample.compose

import android.app.Application
import com.airbnb.lottie.L
import com.airbnb.lottie.sample.compose.dagger.ApplicationComponent
import com.airbnb.lottie.sample.compose.dagger.DaggerApplicationComponent
import com.airbnb.mvrx.Mavericks

class LottieComposeApplication : Application() {
    lateinit var component: ApplicationComponent

    override fun onCreate() {
        super.onCreate()
        Mavericks.initialize(this)
        component = DaggerApplicationComponent.create()
        L.DBG = true
        @Suppress("RestrictedApi")
        L.setTraceEnabled(true)
    }
}

package com.airbnb.lottie.sample.compose

import android.util.Base64
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavController
import androidx.navigation.navArgument

fun NavController.navigate(route: Route) = navigate(route.route)

sealed class Route(val route: String, val args: List<NamedNavArgument> = emptyList()) {
    object Showcase : Route("showcase")

    object Preview : Route("preview")

    object LottieFiles : Route("lottiefiles")

    object Examples : Route("examples")

    object BasicUsageExamples : Route("basic usage examples")

    object AnimatableUsageExamples : Route("LottieAnimatable examples")

    object TransitionsExamples : Route("transitions examples")

    object ViewPagerExample : Route("view pager example")

    object NetworkExamples : Route("network examples")

    object ImagesExamples : Route("image examples")

    object TextExamples : Route("text examples")

    object DynamicPropertiesExamples : Route("dynamic properties examples")

    object ContentScaleExamples : Route("ContentScale examples")

    object CachingExamples : Route("Caching examples")

    object Player : Route(
        "player",
        listOf(
            navArgument("url") {
                androidx.navigation.NavType.StringType
                nullable = true
            },
            navArgument("file") {
                androidx.navigation.NavType.StringType
                nullable = true
            },
            navArgument("asset") {
                androidx.navigation.NavType.StringType
                nullable = true
            },
            navArgument("backgroundColor") {
                androidx.navigation.NavType.StringType
                nullable = true
            },
        )
    ) {
        val fullRoute = "$route?url={url}&file={file}&asset={asset}&backgroundColor={backgroundColor}"

        fun forUrl(url: String, backgroundColor: String? = null) = when (backgroundColor) {
            null -> "${route}?url=${url.toBase64()}"
            else -> "${route}?url=${url.toBase64()}&backgroundColor=${backgroundColor.toBase64()}"
        }

        fun forFile(file: String) = "${route}?file=${file.toBase64()}"

        fun forAsset(asset: String) = "${route}?asset=${asset.toBase64()}"
    }
}

private fun String.toBase64() = Base64.encodeToString(toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)

package com.airbnb.lottie.samples.api

import com.airbnb.lottie.samples.model.AnimationResponse
import com.airbnb.lottie.samples.model.AnimationResponseV2
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface LottiefilesApi {
    @GET("v1/recent")
    suspend fun getRecent(@Query("page") page: Int): AnimationResponse

    @GET("v1/popular")
    suspend fun getPopular(@Query("page") page: Int): AnimationResponse

    @GET("v2/featured")
    suspend fun getCollection(): AnimationResponseV2

    @GET("v1/search/{query}")
    suspend fun search(@Path("query") query: String, @Query("page") page: Int): AnimationResponse
}

package com.airbnb.lottie.samples.model

import android.os.Parcelable
import com.airbnb.lottie.samples.utils.toColorIntSafe
import kotlinx.parcelize.Parcelize

@Parcelize
data class AnimationData(
    val id: Long,
    val title: String,
    val description: String?,
    private val bgColor: String?,
    val preview: String?,
    val lottieLink: String,
    val userInfo: UserInfo?
) : Parcelable {
    val bgColorInt get() = bgColor.toColorIntSafe()
}

package com.airbnb.lottie.samples.model

import android.os.Parcelable
import com.airbnb.lottie.samples.utils.toColorIntSafe
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize

@Parcelize
data class AnimationDataV2(
    @SerializedName("bg_color") val bgColor: String = "",
    @SerializedName("file") val file: String = "",
    @SerializedName("id") val id: Int,
    @SerializedName("preview") val preview: String? = "",
    @SerializedName("title") val title: String = "",
) : Parcelable {
    val bgColorInt get() = bgColor.toColorIntSafe()
}

package com.airbnb.lottie.samples.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// This is a lint bug
@SuppressWarnings("ParcelCreator")
@Parcelize
data class AnimationResponse(
    val currentPage: Int,
    val data: List<AnimationData>,
    val from: String,
    val lastPage: Int,
    val nextPageUrl: String?,
    val path: String,
    val perPage: Int,
    val prevPageUrl: String,
    val to: Int,
    val total: Int
) : Parcelable

package com.airbnb.lottie.samples.model

data class AnimationResponseV2(
    val data: List<AnimationDataV2> = emptyList(),
)

package com.airbnb.lottie.samples.model

import android.annotation.SuppressLint
import android.net.Uri
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize

@SuppressLint("ParcelCreator")
@Parcelize
data class CompositionArgs(
    val assetName: String? = null,
    val url: String? = null,
    val fileUri: Uri? = null,
    val asset: String? = null,
    val animationData: AnimationData? = null,
    val animationDataV2: AnimationDataV2? = null
) : Parcelable {
    @IgnoredOnParcel
    val isJson = (url ?: animationData?.lottieLink ?: animationDataV2?.file)?.endsWith("json") == true
}

package com.airbnb.lottie.samples.model

import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes

data class ShowcaseItem(
    @DrawableRes val drawableRes: Int,
    @StringRes val titleRes: Int,
    val clickListener: (context: Context) -> Unit
)

package com.airbnb.lottie.samples.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// This is a lint bug
@SuppressWarnings("ParcelCreator")
@Parcelize
data class UserInfo(
    val id: Long,
    val name: String,
    val bio: String?,
    val location: String?,
    val city: String?,
    val social_twitter: String?,
    val social_dribbble: String?,
    val social_behance: String?,
    val url: String?
) : Parcelable

package com.airbnb.lottie.samples.utils

import android.app.Activity
import android.view.LayoutInflater
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
 * Create bindings for a view similar to bindView.
 *
 * To use, just call:
 * private val binding: HomeWorkoutDetailsActivityBinding by viewBinding()
 * with your binding class and access it as you normally would.
 */
inline fun <reified T : ViewBinding> Activity.viewBinding() = ActivityViewBindingDelegate(T::class.java, this)

class ActivityViewBindingDelegate<T : ViewBinding>(
    private val bindingClass: Class<T>,
    val activity: Activity
) : ReadOnlyProperty<Activity, T> {
    private var binding: T? = null

    override fun getValue(thisRef: Activity, property: KProperty<*>): T {
        binding?.let { return it }

        val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java)
        @Suppress("UNCHECKED_CAST")
        binding = inflateMethod.invoke(null, thisRef.layoutInflater) as T
        thisRef.setContentView(binding!!.root)
        return binding!!
    }
}

package com.airbnb.lottie.samples.utils

import android.os.Bundle
import android.view.View
import com.airbnb.epoxy.AsyncEpoxyController
import com.airbnb.epoxy.EpoxyController
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.databinding.BaseFragmentBinding
import com.airbnb.mvrx.BaseMvRxFragment

private class BaseEpoxyController(
    private val buildModelsCallback: EpoxyController.() -> Unit
) : AsyncEpoxyController() {
    override fun buildModels() {
        buildModelsCallback()
    }
}

abstract class BaseEpoxyFragment : BaseMvRxFragment(R.layout.base_fragment) {
    protected val binding: BaseFragmentBinding by viewBinding()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.recyclerView.setController(BaseEpoxyController { buildModels() })
    }

    override fun invalidate() {
        binding.recyclerView.requestModelBuild()
    }

    abstract fun EpoxyController.buildModels()
}

package com.airbnb.lottie.samples.utils

import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
 * Create bindings for a view similar to bindView.
 *
 * To use, just call
 * private val binding: FHomeWorkoutDetailsBinding by viewBinding()
 * with your binding class and access it as you normally would.
 */
inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java, this)

class FragmentViewBindingDelegate<T : ViewBinding>(
    bindingClass: Class<T>,
    val fragment: Fragment
) : ReadOnlyProperty<Fragment, T> {
    private val clearBindingHandler by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) }
    private var binding: T? = null

    private val bindMethod = bindingClass.getMethod("bind", View::class.java)

    init {
        fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
            viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
                @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
                fun onDestroy() {
                    // Lifecycle listeners are called before onDestroyView in a Fragment.
                    // However, we want views to be able to use bindings in onDestroyView
                    // to do cleanup so we clear the reference one frame later.
                    clearBindingHandler.post { binding = null }
                }
            })
        }
    }

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        binding?.let { return it }

        val lifecycle = fragment.viewLifecycleOwner.lifecycle
        if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
            error("Cannot access view bindings. View lifecycle is ${lifecycle.currentState}!")
        }

        binding = bindMethod.invoke(null, thisRef.requireView()) as T
        return binding!!
    }
}

package com.airbnb.lottie.samples.utils

import androidx.lifecycle.viewModelScope
import com.airbnb.lottie.samples.BuildConfig
import com.airbnb.mvrx.*
import kotlinx.coroutines.*

abstract class MvRxViewModel<S : MvRxState>(initialState: S) : BaseMvRxViewModel<S>(initialState, BuildConfig.DEBUG) {
    /**
     * This uses [Dispatchers.Main.immediate] by default to mimic [viewModelScope].
     */
    fun <T : Any?> (suspend () -> T).execute(
        dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
        reducer: S.(Async<T>) -> S
    ): Job {
        setState { reducer(Loading()) }
        return viewModelScope.launch(dispatcher) {
            try {
                val result = invoke()
                setState { reducer(Success(result)) }
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                setState { reducer(Fail(e)) }
            }
        }
    }
}

package com.airbnb.lottie.samples.utils

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.airbnb.lottie.L
import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar

fun Fragment.startActivity(cls: Class<*>) {
    startActivity(Intent(context, cls))
}

fun String.urlIntent(): Intent =
    Intent(Intent.ACTION_VIEW).setData(Uri.parse(this))

fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = true): View =
    LayoutInflater.from(context).inflate(layout, this, attachToRoot)

fun String.hasPermission(context: Context): Boolean =
    ContextCompat.checkSelfPermission(context, this) == PackageManager.PERMISSION_GRANTED

fun TextView.setDrawableLeft(@DrawableRes drawableRes: Int, activity: Activity) {
    val drawable = VectorDrawableCompat.create(resources, drawableRes, activity.theme)
    setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
}

fun View.showSnackbarLong(@StringRes message: Int) =
    showSnackbarLong(resources.getString(message))

fun View.showSnackbarLong(message: String) =
    Snackbar.make(this, message, Snackbar.LENGTH_LONG).show()

fun View.setVisibleIf(condition: Boolean) {
    visibility = if (condition) View.VISIBLE else View.GONE
}

fun ImageView.setImageUrl(url: String?) = url?.let { Glide.with(this).load(it).into(this) }

inline fun <reified T> flatten(vararg lists: List<T>?) = lists.flatMap { it ?: emptyList() }

fun Float.lerp(other: Float, amount: Float): Float = this + amount * (other - this)

fun Float.sqrt() = kotlin.math.sqrt(this.toDouble()).toFloat()

fun View.getText(@StringRes res: Int) = this.resources.getText(res)
operator fun Boolean.inc() = !this

fun Context.hasPermission(permission: String): Boolean {
    return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}

fun Vibrator.vibrateCompat(millis: Long) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        vibrate(VibrationEffect.createOneShot(millis, VibrationEffect.DEFAULT_AMPLITUDE))
    } else {
        @Suppress("DEPRECATION")
        vibrate(millis)
    }
}

@ColorInt
fun String?.toColorIntSafe(): Int {
    var bgColor = this ?: "#ffffff"
    bgColor = if (bgColor.startsWith("#")) bgColor else "#$bgColor"

    return try {
        when (bgColor.length) {
            0 -> "#ffffff"
            4 -> "#%c%c%c%c%c%c".format(
                bgColor[1], bgColor[1],
                bgColor[2], bgColor[2],
                bgColor[3], bgColor[3]
            )
            5 -> "#%c%c%c%c%c%c%c%c".format(
                bgColor[1], bgColor[1],
                bgColor[2], bgColor[2],
                bgColor[3], bgColor[3],
                bgColor[4], bgColor[4]
            )
            else -> bgColor
        }.toColorInt()
    } catch (e: IllegalArgumentException) {
        Log.w(L.TAG, "Unable to parse $bgColor.")
        Color.WHITE
    }
}

fun Context.hideKeyboard() {
    val inputMethodManager = getSystemService<InputMethodManager>()!!
    inputMethodManager.hideSoftInputFromWindow((this as Activity).currentFocus?.windowToken, 0)
}

package com.airbnb.lottie.samples.utils

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
 * Create bindings for a view similar to bindView.
 *
 * To use, just call:
 * private val binding: FHomeWorkoutDetailsBinding by viewBinding()
 * with your binding class and access it as you normally would.
 */
inline fun <reified T : ViewBinding> ViewGroup.viewBinding() = ViewBindingDelegate(T::class.java, this)

class ViewBindingDelegate<T : ViewBinding>(
    private val bindingClass: Class<T>,
    val view: ViewGroup
) : ReadOnlyProperty<ViewGroup, T> {
    private var binding: T? = null

    override fun getValue(thisRef: ViewGroup, property: KProperty<*>): T {
        binding?.let { return it }

        @Suppress("UNCHECKED_CAST")
        binding = try {
            val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java)
            inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), thisRef)
        } catch (e: NoSuchMethodException) {
            val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
            inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), thisRef, true) as T
        } as T
        return binding!!
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.TextProp
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.databinding.ItemViewShowcaseAnimationBinding
import com.airbnb.lottie.samples.utils.setImageUrl
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class AnimationItemView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val binding: ItemViewShowcaseAnimationBinding by viewBinding()

    @ModelProp
    fun setPreviewUrl(url: String?) {
        binding.imageView.setImageUrl(url)
    }

    @TextProp
    fun setTitle(title: CharSequence?) {
        binding.titleView.text = title
    }

    @ModelProp
    fun setPreviewBackgroundColor(@ColorInt bgColor: Int?) {
        if (bgColor == null) {
            binding.imageView.setBackgroundResource(R.color.loading_placeholder)
            binding.imageView.setImageDrawable(null)
        } else {
            binding.imageView.setBackgroundColor(bgColor)
        }
    }

    @ModelProp(options = [ModelProp.Option.DoNotHash])
    override fun setOnClickListener(l: OnClickListener?) {
        binding.container.setOnClickListener(l)
    }
}

package com.airbnb.lottie.samples.views

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import com.airbnb.lottie.samples.R

class BackgroundColorView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleInt: Int = 0
) : View(context, attrs, defStyleInt) {

    private val paint = Paint().apply {
        isAntiAlias = true
    }

    @SuppressLint("MissingSuperCall")
    override fun draw(canvas: Canvas) {
        if (background !is ColorDrawable) return

        val cx = canvas.width / 2f
        val cy = canvas.height / 2f
        val r = cx.coerceAtMost(cy)
        if (getColor() == Color.WHITE) {
            paint.strokeWidth =
                resources.getDimensionPixelSize(R.dimen.background_color_view_stroke_width).toFloat()
            paint.style = Paint.Style.STROKE
            paint.color = ContextCompat.getColor(context, R.color.background_color1_stroke)
        } else {
            paint.color = getColor()
            paint.style = Paint.Style.FILL
        }
        canvas.drawCircle(cx, cy, r - paint.strokeWidth, paint)
    }

    @ColorInt
    fun getColor() = (background as ColorDrawable).color
}

package com.airbnb.lottie.samples.views

import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.lottie.samples.databinding.ItemViewBottomSheetBinding
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class BottomSheetItemView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val binding: ItemViewBottomSheetBinding by viewBinding()

    @SuppressLint("SetTextI18n")
    fun set(left: String, right: String? = null) {
        binding.leftTextView.text = left
        binding.rightTextView.isVisible = !TextUtils.isEmpty(right)
        binding.rightTextView.text = right
    }

    @ModelProp
    fun setText(text: String) {
        set(text)
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.databinding.ItemViewControlBarBinding
import com.airbnb.lottie.samples.utils.getText
import com.airbnb.lottie.samples.utils.viewBinding

class ControlBarItemToggleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val binding: ItemViewControlBarBinding by viewBinding()

    init {
        orientation = HORIZONTAL
        setBackgroundResource(R.drawable.control_bar_item_view_background)
        setPadding(resources.getDimensionPixelSize(R.dimen.control_bar_button_padding))
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(it, R.styleable.ControlBarItemToggleView, 0, 0)

            val textRes = typedArray.getResourceId(R.styleable.ControlBarItemToggleView_text, 0)
            if (textRes != 0) {
                binding.textView.text = getText(textRes)
            }

            val drawableRes = typedArray.getResourceId(R.styleable.ControlBarItemToggleView_src, 0)
            if (drawableRes == 0) {
                binding.imageView.isVisible = false
            } else {
                binding.imageView.setImageResource(drawableRes)
            }

            typedArray.recycle()
        }
    }

    override fun childDrawableStateChanged(child: View) {
        super.childDrawableStateChanged(child)
        if (child is ImageView && child.drawable != null) {
            val color =
                if (child.isActivated) Color.WHITE
                else ContextCompat.getColor(context, R.color.control_bar_content_unactivated)
            DrawableCompat.setTint(child.drawable.mutate(), color)
        }
    }

    fun getText() = binding.textView.text.toString()

    fun setText(text: String) {
        binding.textView.text = text
    }

    fun setImageResource(@DrawableRes drawableRes: Int) {
        binding.imageView.setImageResource(drawableRes)
        childDrawableStateChanged(binding.imageView)
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import androidx.customview.widget.ViewDragHelper

class InterceptingFrameLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    var viewDragHelper: ViewDragHelper? = null

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (viewDragHelper?.shouldInterceptTouchEvent(ev) == true) return true
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        viewDragHelper?.processTouchEvent(event)
        return true
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.airbnb.lottie.samples.databinding.ListingCardBinding
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class ListingCard @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val binding: ListingCardBinding by viewBinding()

    @CallbackProp
    fun onToggled(listener: ((isWishListed: Boolean) -> Unit)?) {
        binding.wishListIcon.setOnClickListener(when (listener) {
            null -> null
            else -> { _ ->
                listener(binding.wishListIcon.progress == 0f)
            }
        })
    }

    @ModelProp
    fun isWishListed(isWishListed: Boolean) {
        val targetProgress = if (isWishListed) 1f else 0f
        binding.wishListIcon.speed = if (isWishListed) 1f else -1f
        if (binding.wishListIcon.progress != targetProgress) {
            binding.wishListIcon.playAnimation()
        }
    }

    @OnViewRecycled
    fun onRecycled() {
        binding.wishListIcon.pauseAnimation()
        binding.wishListIcon.progress = 0f
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.airbnb.epoxy.ModelView
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.utils.inflate

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class LoadingView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    init {
        inflate(R.layout.list_item_loader)
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.lottie.samples.LottiefilesMode
import com.airbnb.lottie.samples.databinding.LottiefilesTabBarBinding
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class LottiefilesTabBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val binding: LottiefilesTabBarBinding by viewBinding()

    @ModelProp
    fun setMode(mode: LottiefilesMode) {
        binding.popularView.isActivated = mode == LottiefilesMode.Popular
        binding.recentView.isActivated = mode == LottiefilesMode.Recent
        binding.searchView.isActivated = mode == LottiefilesMode.Search
    }

    @ModelProp(options = [ModelProp.Option.DoNotHash])
    fun setPopularClickListener(listener: OnClickListener) {
        binding.popularView.setOnClickListener(listener)
    }

    @ModelProp(options = [ModelProp.Option.DoNotHash])
    fun setRecentClickListener(listener: OnClickListener) {
        binding.recentView.setOnClickListener(listener)
    }

    @ModelProp(options = [ModelProp.Option.DoNotHash])
    fun setSearchClickListener(listener: OnClickListener) {
        binding.searchView.setOnClickListener(listener)
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.TextProp
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.databinding.MarqueeBinding
import com.airbnb.lottie.samples.utils.getText
import com.airbnb.lottie.samples.utils.setVisibleIf
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class Marquee @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val binding: MarqueeBinding by viewBinding()

    init {
        orientation = VERTICAL
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(it, R.styleable.Marquee, 0, 0)

            val titleRes = typedArray.getResourceId(R.styleable.Marquee_titleText, 0)
            if (titleRes != 0) {
                setTitle(getText(titleRes))
            }

            val subtitleRes = typedArray.getResourceId(R.styleable.Marquee_subtitleText, 0)
            if (subtitleRes != 0) {
                setSubtitle(getText(subtitleRes))
            }

            typedArray.recycle()
        }
    }

    @TextProp
    fun setTitle(title: CharSequence) {
        binding.titleView.text = title
    }

    @TextProp
    fun setSubtitle(subtitle: CharSequence?) {
        binding.subtitleView.text = subtitle
        binding.subtitleView.setVisibleIf(!subtitle.isNullOrEmpty())
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.widget.doAfterTextChanged
import com.airbnb.lottie.samples.databinding.ItemViewSearchInputBinding
import com.airbnb.lottie.samples.utils.viewBinding
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class SearchInputItemView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val binding: ItemViewSearchInputBinding by viewBinding()

    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query

    init {
        binding.searchEditText.doAfterTextChanged { text ->
            _query.value = text?.toString() ?: ""
        }
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.TextProp
import com.airbnb.lottie.samples.databinding.SectionHeaderViewBinding
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class SectionHeaderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val binding: SectionHeaderViewBinding by viewBinding()

    @TextProp
    fun setTitle(title: CharSequence) {
        binding.titleView.text = title
    }

    @ModelProp(options = [ModelProp.Option.DoNotHash])
    override fun setOnClickListener(listener: OnClickListener?) {
        super.setOnClickListener(listener)
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.model.ShowcaseItem

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class ShowcaseCarousel @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : EpoxyRecyclerView(context, attrs, defStyleAttr), EpoxyRecyclerView.ModelBuilderCallback {

    private var items: List<ShowcaseItem>? = null

    init {
        layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
        buildModelsWith(this)
        val sidePadding = resources.getDimensionPixelSize(R.dimen.showcase_carousel_padding)
        setPadding(sidePadding, 0, sidePadding, 0)
    }

    @ModelProp
    fun setShowcaseItems(items: List<ShowcaseItem>) {
        this.items = items
        requestModelBuild()
    }

    override fun buildModels(controller: EpoxyController) {
        items?.forEach {
            ShowcaseDemoItemViewModel_()
                .id(it.titleRes)
                .showcaseItem(it)
                .addTo(controller)
        }
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.lottie.samples.databinding.ItemViewShowcaseDemoBinding
import com.airbnb.lottie.samples.model.ShowcaseItem
import com.airbnb.lottie.samples.utils.viewBinding

@ModelView(autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT)
class ShowcaseDemoItemView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    private val binding: ItemViewShowcaseDemoBinding by viewBinding()

    @ModelProp
    fun setShowcaseItem(item: ShowcaseItem) {
        binding.imageView.setImageResource(item.drawableRes)

        binding.titleView.text = resources.getText(item.titleRes)

        binding.cardView.setOnClickListener { item.clickListener(this.context.applicationContext) }
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.core.content.withStyledAttributes
import com.airbnb.lottie.samples.R
import com.airbnb.lottie.samples.databinding.TabItemBinding
import com.airbnb.lottie.samples.utils.viewBinding

class TabBarItemView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val binding: TabItemBinding by viewBinding()

    init {
        orientation = VERTICAL

        context.withStyledAttributes(attrs, R.styleable.TabBarItemView) {
            if (hasValue(R.styleable.TabBarItemView_titleText)) {
                binding.titleView.text = getText(R.styleable.TabBarItemView_titleText)
            }
        }
    }
}

package com.airbnb.lottie.samples.views

import android.content.Context
import android.util.AttributeSet
import com.airbnb.lottie.LottieAnimationView

class WishListIconView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LottieAnimationView(context, attrs, defStyleAttr) {

    fun toggleWishlisted() {
        isActivated = !isActivated
    }

    override fun setActivated(activated: Boolean) {
        super.setActivated(activated)
        speed = if (activated) 1f else -2f
        progress = 0f
        playAnimation()
    }
}

package com.airbnb.lottie.sample.compose.benchmark

import android.content.Context
import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime

@RunWith(AndroidJUnit4::class)
class LottieBaselineBenchmark {

    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    private lateinit var context: Context
    private lateinit var device: UiDevice

    @Before
    fun setUp() {
        val instrumentation = InstrumentationRegistry.getInstrumentation()
        context = ApplicationProvider.getApplicationContext()
        device = UiDevice.getInstance(instrumentation)
    }

    @Test
    fun benchmarkNoCompilation() {
        benchmark(CompilationMode.None())
    }

    @Test
    fun benchmarkBaselineProfiles() {
        benchmark(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require))
    }

    @OptIn(ExperimentalTime::class)
    fun benchmark(compilationMode: CompilationMode) {
        benchmarkRule.measureRepeated(
            packageName = PACKAGE_NAME,
            metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
            iterations = 1,
            startupMode = StartupMode.COLD,
            compilationMode = compilationMode,
            setupBlock = {
                pressHome()

            }
        ) {
            startActivityAndWait()
            // Find the ScrollView in the Showcase route
            val selector = UiSelector().className("android.widget.ScrollView")
            val scrollable = UiScrollable(selector)
            scrollable.waitUntilReady()
            for (i in 0 until scrollable.childCount) {
                val childSelector = UiSelector()
                    .enabled(true)
                    .clickable(true)
                    .instance(i)
                val child = scrollable.getChild(childSelector)
                scrollable.scrollIntoView(child)
                child.clickAndWait()
            }
        }
    }

    @OptIn(ExperimentalTime::class)
    private fun UiScrollable.waitUntilReady() {
        this.waitUntil {
            // We know that there are at least 9 children
            childCount >= EXPECTED_ITEM_INDEX_COUNT
        }
    }

    @OptIn(ExperimentalTime::class)
    private fun UiObject.clickAndWait(maxWaitTime: Duration = Duration.seconds(5)) {
        val maxWaitTimeMs = maxWaitTime.toLong(DurationUnit.MILLISECONDS)
        click()
        device.waitForIdle(maxWaitTimeMs)
        Thread.sleep(maxWaitTimeMs)
        device.pressBack()
    }

    @OptIn(ExperimentalTime::class)
    private fun <T> T.waitUntil(maxWaitTime: Duration = Duration.seconds(5), condition: (T) -> Boolean) {
        var waitTime = 0L
        val maxWaitTimeMs = maxWaitTime.toLong(DurationUnit.MILLISECONDS)
        val incrementalDelay = 150L
        while (waitTime <= maxWaitTimeMs) {
            val ready = condition(this)
            if (ready) {
                break
            }
            Thread.sleep(incrementalDelay)
            waitTime += incrementalDelay
        }
    }

    companion object {
        private const val PACKAGE_NAME = "com.airbnb.lottie.sample.compose"
        private const val EXPECTED_ITEM_INDEX_COUNT = 9
    }
}

package com.airbnb.lottie.sample.compose.benchmark

import android.content.Context
import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime

@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalBaselineProfilesApi::class)
class LottieBaselineProfiles {

    @get:Rule
    val baselineProfileRule = BaselineProfileRule()

    private lateinit var context: Context
    private lateinit var device: UiDevice

    @Before
    fun setUp() {
        val instrumentation = InstrumentationRegistry.getInstrumentation()
        context = ApplicationProvider.getApplicationContext()
        device = UiDevice.getInstance(instrumentation)
    }

    @Test
    @OptIn(ExperimentalTime::class)
    fun baselineProfiles() {
        baselineProfileRule.collectBaselineProfile(
            packageName = PACKAGE_NAME,
        ) {
            pressHome()
            startActivityAndWait()
            // Find the ScrollView in the Showcase route
            val selector = UiSelector().className("android.widget.ScrollView")
            val scrollable = UiScrollable(selector)
            scrollable.waitUntilReady()
            for (i in 0 until scrollable.childCount) {
                val childSelector = UiSelector()
                    .enabled(true)
                    .clickable(true)
                    .instance(i)
                val child = scrollable.getChild(childSelector)
                scrollable.scrollIntoView(child)
                child.clickAndWait()
            }
        }
    }

    @OptIn(ExperimentalTime::class)
    private fun UiScrollable.waitUntilReady() {
        this.waitUntil {
            // We know that there are at least 9 children
            childCount >= EXPECTED_ITEM_INDEX_COUNT
        }
    }

    @OptIn(ExperimentalTime::class)
    private fun UiObject.clickAndWait(maxWaitTime: Duration = Duration.seconds(5)) {
        val maxWaitTimeMs = maxWaitTime.toLong(DurationUnit.MILLISECONDS)
        click()
        device.waitForIdle(maxWaitTimeMs)
        Thread.sleep(maxWaitTimeMs)
        device.pressBack()
    }

    @OptIn(ExperimentalTime::class)
    private fun <T> T.waitUntil(maxWaitTime: Duration = Duration.seconds(5), condition: (T) -> Boolean) {
        var waitTime = 0L
        val maxWaitTimeMs = maxWaitTime.toLong(DurationUnit.MILLISECONDS)
        val incrementalDelay = 150L
        while (waitTime <= maxWaitTimeMs) {
            val ready = condition(this)
            if (ready) {
                break
            }
            Thread.sleep(incrementalDelay)
            waitTime += incrementalDelay
        }
    }

    companion object {
        private const val PACKAGE_NAME = "com.airbnb.lottie.sample.compose"
        private const val EXPECTED_ITEM_INDEX_COUNT = 9
    }
}

package com.airbnb.lottie.sample.compose.api

data class AnimationDataV1(
    val id: Int,
    val bg_color: String?,
    val preview: String?,
    val title: String,
    val lottie_link: String
)

package com.airbnb.lottie.sample.compose.api

import com.airbnb.lottie.sample.compose.ui.toColorSafe

data class AnimationDataV2(
    val id: Int,
    val bg_color: String?,
    val preview_url: String?,
    val title: String,
    val file: String
) {

    constructor(data: AnimationDataV1) : this(
        data.id,
        data.bg_color,
        data.preview,
        data.title,
        data.lottie_link,
    )

    val bgColor get() = bg_color.toColorSafe()
}

package com.airbnb.lottie.sample.compose.api

data class AnimationsResponseV1(
    val data: List<AnimationDataV1>,
    val current_page: Int,
    val last_page: Int
)

package com.airbnb.lottie.sample.compose.api

data class AnimationsResponseV2(
    val data: List<AnimationDataV2>,
    val current_page: Int,
    val last_page: Int
)

package com.airbnb.lottie.sample.compose.api

import dagger.Module
import dagger.Provides
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create
import javax.inject.Singleton

@Module
object ApiModule {
    @Provides
    @Singleton
    fun providesRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.lottiefiles.com/")
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun providesLottieFilesApi(retrofit: Retrofit): LottieFilesApi {
        return retrofit.create()
    }
}

package com.airbnb.lottie.sample.compose.api

import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface LottieFilesApi {
    @GET("v2/featured")
    suspend fun getFeatured(): AnimationsResponseV2

    @GET("v1/recent")
    suspend fun getRecent(@Query("page") page: Int): AnimationsResponseV1

    @GET("v1/popular")
    suspend fun getPopular(@Query("page") page: Int): AnimationsResponseV1

    @GET("v1/search/{query}")
    suspend fun search(@Path("query") query: String, @Query("page") page: Int): AnimationsResponseV1
}

package com.airbnb.lottie.sample.compose.composables

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberImagePainter

@Composable
fun AnimationRow(
    title: String,
    previewUrl: String,
    previewBackgroundColor: Color,
    onClick: () -> Unit
) {
    Surface(
        onClick = onClick,
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Image(
                painter = rememberImagePainter(previewUrl),
                modifier = Modifier
                    .padding(end = 16.dp)
                    .size(40.dp)
                    .background(color = previewBackgroundColor),
                contentDescription = null
            )
            Text(
                title,
                fontSize = 16.sp,
                overflow = TextOverflow.Ellipsis
            )
        }

    }
}

package com.airbnb.lottie.sample.compose.composables

import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.unit.Dp
import kotlinx.coroutines.delay

@Composable
fun DebouncedCircularProgressIndicator(
    modifier: Modifier = Modifier,
    delayMs: Long = 1_500L,
    color: Color = MaterialTheme.colors.primary,
    strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth,
) {
    var readyToShow by remember { mutableStateOf(false) }
    LaunchedEffect(readyToShow) {
        delay(delayMs)
        readyToShow = true
    }
    if (readyToShow) {
        CircularProgressIndicator(
            color = color,
            strokeWidth = strokeWidth,
            modifier = modifier
        )
    }
}

package com.airbnb.lottie.sample.compose.composables

import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.sample.compose.R

@Composable
fun Loader(
    modifier: Modifier = Modifier
) {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading))
    LottieAnimation(
        composition,
        iterations = LottieConstants.IterateForever,
        modifier = modifier
            .size(100.dp)
    )
}

package com.airbnb.lottie.sample.compose.composables

import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.sample.compose.ui.TealDark

@Composable
fun Marquee(text: String) {
    Text(
        text,
        fontSize = 32.sp,
        color = TealDark,
        fontWeight = FontWeight.Bold,
        modifier = Modifier
            .padding(vertical = 24.dp, horizontal = 16.dp)
    )
}

package com.airbnb.lottie.sample.compose.dagger

import com.airbnb.lottie.sample.compose.api.ApiModule
import com.airbnb.mvrx.MavericksViewModel
import dagger.Component
import javax.inject.Singleton

@Component(
    modules = [
        ApiModule::class,
        AppModule::class
    ]
)
@Singleton
interface ApplicationComponent {
    fun viewModelFactories(): Map<Class<out MavericksViewModel<*>>, AssistedViewModelFactory<*, *>>
}

package com.airbnb.lottie.sample.compose.dagger

import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel

interface AssistedViewModelFactory<VM : MavericksViewModel<S>, S : MavericksState> {
    fun create(initialState: S): VM
}

package com.airbnb.lottie.sample.compose.dagger

import androidx.activity.ComponentActivity
import com.airbnb.lottie.sample.compose.LottieComposeApplication
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext

inline fun <reified VM : MavericksViewModel<S>, S : MavericksState> daggerMavericksViewModelFactory() =
    DaggerMavericksViewModelFactory<VM, S>(VM::class.java)

class DaggerMavericksViewModelFactory<VM : MavericksViewModel<S>, S : MavericksState>(
    private val viewModelClass: Class<out MavericksViewModel<S>>
) : MavericksViewModelFactory<VM, S> {

    override fun create(viewModelContext: ViewModelContext, state: S): VM {
        return createViewModel(viewModelContext.activity, state)
    }

    private fun <VM : MavericksViewModel<S>, S : MavericksState> createViewModel(
        fragmentActivity: ComponentActivity,
        initialState: S
    ): VM {
        val viewModelFactoryMap = (fragmentActivity.application as LottieComposeApplication).component.viewModelFactories()
        val viewModelFactory = viewModelFactoryMap[viewModelClass]

        @Suppress("UNCHECKED_CAST")
        val castedViewModelFactory = viewModelFactory as? AssistedViewModelFactory<VM, S>
        val viewModel = castedViewModelFactory?.create(initialState)
        return viewModel as VM
    }
}

package com.airbnb.lottie.sample.compose.dagger

import com.airbnb.lottie.sample.compose.lottiefiles.LottieFilesRecentAndPopularViewModel
import com.airbnb.lottie.sample.compose.lottiefiles.LottieFilesSearchViewModel
import com.airbnb.lottie.sample.compose.showcase.ShowcaseViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap

@Module
interface AppModule {

    @Binds
    @IntoMap
    @ViewModelKey(ShowcaseViewModel::class)
    fun showcaseViewModelFactory(factory: ShowcaseViewModel.Factory): AssistedViewModelFactory<*, *>

    @Binds
    @IntoMap
    @ViewModelKey(LottieFilesSearchViewModel::class)
    fun lottieFilesSearchViewModelFactory(factory: LottieFilesSearchViewModel.Factory): AssistedViewModelFactory<*, *>

    @Binds
    @IntoMap
    @ViewModelKey(LottieFilesRecentAndPopularViewModel::class)
    fun lottieFilesRecentAndPopularViewModelFactory(factory: LottieFilesRecentAndPopularViewModel.Factory): AssistedViewModelFactory<*, *>

}

package com.airbnb.lottie.sample.compose.dagger

import com.airbnb.mvrx.MavericksViewModel
import dagger.MapKey
import kotlin.reflect.KClass

/**
 * A [MapKey] for populating a map of ViewModels and their factories.
 */
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@MapKey
annotation class ViewModelKey(val value: KClass<out MavericksViewModel<*>>)

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.rememberLottieAnimatable
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.sample.compose.R

@Composable
fun AnimatableExamplesPage() {
    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())
        ) {
            ExampleCard("Example 1", "Repeat Forever") {
                Example1()
            }
            ExampleCard("Example 2", "Draggable Progress Slider") {
                Example2()
            }
            ExampleCard("Example 3", "Draggable Speed Slider") {
                Example3()
            }
            ExampleCard("Example 4", "Repeat once. Click to repeat again") {
                Example4()
            }
            ExampleCard("Example 5", "Click to toggle playback") {
                Example5()
            }
        }
    }
}

@Composable
private fun Example1() {
    val anim = rememberLottieAnimatable()
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    LaunchedEffect(composition) {
        anim.animate(
            composition,
            iterations = LottieConstants.IterateForever,
        )
    }
    LottieAnimation(anim.composition, anim.progress)
}

@Composable
private fun Example2() {
    val anim = rememberLottieAnimatable()
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    var sliderGestureProgress: Float? by remember { mutableStateOf(null) }
    LaunchedEffect(composition, sliderGestureProgress) {
        when (val p = sliderGestureProgress) {
            null -> anim.animate(
                composition,
                iterations = LottieConstants.IterateForever,
                initialProgress = anim.progress,
                continueFromPreviousAnimate = false,
            )
            else -> anim.snapTo(progress = p)
        }
    }
    Box {
        LottieAnimation(anim.composition, anim.progress)
        Slider(
            value = sliderGestureProgress ?: anim.progress,
            onValueChange = { sliderGestureProgress = it },
            onValueChangeFinished = { sliderGestureProgress = null },
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp)
        )
    }
}

@Composable
private fun Example3() {
    val anim = rememberLottieAnimatable()
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    var speed by remember { mutableStateOf(1f) }
    LaunchedEffect(composition, speed) {
        anim.animate(
            composition,
            iterations = LottieConstants.IterateForever,
            speed = speed,
            initialProgress = anim.progress,
        )
    }
    Box {
        LottieAnimation(composition, anim.progress)
        Slider(
            value = speed,
            onValueChange = { speed = it },
            valueRange = -3f..3f,
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp)
        )
        Box(
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 24.dp)
                .size(width = 1.dp, height = 16.dp)
                .background(Color.Black)
        )
    }
}

@Composable
private fun Example4() {
    var nonce by remember { mutableStateOf(1) }
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    val animatable = rememberLottieAnimatable()

    LaunchedEffect(composition, nonce) {
        composition ?: return@LaunchedEffect
        animatable.animate(
            composition,
            continueFromPreviousAnimate = false,
        )
    }
    LottieAnimation(
        composition,
        animatable.progress,
        modifier = Modifier
            .clickable { nonce++ }
    )
}

@Composable
private fun Example5() {
    var shouldPlay by remember { mutableStateOf(true) }
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    val animatable = rememberLottieAnimatable()

    LaunchedEffect(composition, shouldPlay) {
        if (composition == null || !shouldPlay) return@LaunchedEffect
        animatable.animate(composition, iteration = LottieConstants.IterateForever)
    }
    LottieAnimation(
        composition,
        animatable.progress,
        modifier = Modifier
            .clickable { shouldPlay = !shouldPlay }
    )
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieClipSpec
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R

@Composable
fun BasicUsageExamplesPage() {
    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())
        ) {
            Box(modifier = Modifier.height(16.dp))
            ExampleCard("Example 1", "Repeat once") {
                Example1()
            }
            ExampleCard("Example 2", "Repeat forever") {
                Example2()
            }
            ExampleCard("Example 3", "Repeat forever from 50% to 75%") {
                Example3()
            }
            ExampleCard("Example 4", "Using LottieAnimationResult") {
                Example4()
            }
            ExampleCard("Example 5", "Using LottieComposition") {
                Example5()
            }
            ExampleCard("Example 6", "Splitting out the animation driver") {
                Example6()
            }
            ExampleCard("Example 7", "Toggle on click - click me") {
                Example7()
            }
        }
    }
}

/**
 * Nice and easy... This will play one time as soon as the composition loads
 * then it will stop.
 */
@Composable
private fun Example1() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    LottieAnimation(composition)
}

/**
 * This will repeat forever.
 */
@Composable
private fun Example2() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    LottieAnimation(
        composition,
        iterations = LottieConstants.IterateForever,
    )
}

/**
 * This will repeat between 50% and 75% forever.
 */
@Composable
private fun Example3() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    LottieAnimation(
        composition,
        iterations = LottieConstants.IterateForever,
        clipSpec = LottieClipSpec.Progress(0.5f, 0.75f),
    )
}

/**
 * Here, you can check the result for loading/failure states.
 */
@Composable
private fun Example4() {
    val compositionResult = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    when {
        compositionResult.isLoading -> {
            Text("Animation is loading...")
        }
        compositionResult.isFailure -> {
            Text("Animation failed to load")
        }
        compositionResult.isSuccess -> {
            LottieAnimation(
                compositionResult.value,
                iterations = LottieConstants.IterateForever,
            )
        }
    }
}

/**
 * If you just want access to the composition itself, you can use the delegate
 * version of lottieComposition like this.
 */
@Composable
private fun Example5() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    LottieAnimation(
        composition,
        progress = 0.65f,
    )
}

/**
 * Here, you have access to the composition and animation individually.
 */
@Composable
private fun Example6() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    val progress by animateLottieCompositionAsState(
        composition,
        iterations = LottieConstants.IterateForever,
    )
    LottieAnimation(
        composition,
        progress,
    )
}

/**
 * Here, you can toggle playback by clicking the animation.
 */
@Composable
private fun Example7() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    var isPlaying by remember { mutableStateOf(false) }
    LottieAnimation(
        composition,
        iterations = LottieConstants.IterateForever,
        // When this is true, it it will start from 0 every time it is played again.
        // When this is false, it will resume from the progress it was pause at.
        restartOnPlay = false,
        isPlaying = isPlaying,
        modifier = Modifier
            .clickable { isPlaying = !isPlaying }
    )
}

@Preview
@Composable
fun ExampleCardPreview() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    ExampleCard("Example 1", "Heart animation") {
        LottieAnimation(composition)
    }
}

package com.airbnb.lottie.sample.compose.examples

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.getValue
import androidx.compose.ui.Modifier
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R

@Composable
fun CachingExamplesPage() {
    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())
        ) {
            ExampleCard("Default Caching", "Lottie caches compositions by default") {
                Example1()
            }
            ExampleCard("Day/Night", "Animations in raw/res will automatically respect day and night mode") {
                Example2()
            }
            ExampleCard("Skip Cache", "Skip the cache") {
                Example3()
            }
        }
    }
}

@Composable
private fun Example1() {
    // By default, Lottie will cache compositions with a key derived from your LottieCompositionSpec.
    // If you request the composition multiple times or request it again at some point later, it
    // will return the previous composition. LottieComposition itself it stateless. All stateful
    // actions should happen within LottieAnimation.
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
    LottieAnimation(composition)
}

@Composable
private fun Example2() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.sun_moon))
    LottieAnimation(composition)
}

@Composable
private fun Example3() {
    val composition by rememberLottieComposition(
        LottieCompositionSpec.RawRes(R.raw.we_accept_inline_image),
        // Don't cache this composition. You may want to do this for animations that have images
        // because the bitmaps are much larger to store than the rest of the animation.
        cacheKey = null,
    )
    LottieAnimation(composition)
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.ArrowDropUp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R

@Composable
fun ContentScaleExamplesPage() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.gradient))
    var alignment by remember { mutableStateOf(Alignment.Center) }
    var contentScale by remember { mutableStateOf(ContentScale.Fit) }

    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
        ) {
            OptionsRow(
                alignment,
                contentScale,
                onAlignmentChanged = { alignment = it },
                onContentScaleChanged = { contentScale = it },
            )
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(8.dp)
                    .border(2.dp, Color.Green)
                    .padding(2.dp)
            ) {
                LottieAnimation(
                    composition,
                    progress = 0f,
                    alignment = alignment,
                    contentScale = contentScale,
                )
            }
        }
    }
}
@Composable
private fun OptionsRow(
    alignment: Alignment,
    contentScale: ContentScale,
    onContentScaleChanged: (ContentScale) -> Unit,
    onAlignmentChanged: (Alignment) -> Unit,
) {
    var scaleExpanded by remember { mutableStateOf(false) }
    var alignmentExpanded by remember { mutableStateOf(false) }

    val onContentScaleChangedState by rememberUpdatedState(onContentScaleChanged)
    val onAlignmentChangedState by rememberUpdatedState(onAlignmentChanged)

    Row(
        horizontalArrangement = Arrangement.SpaceAround,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .height(64.dp)
    ) {
        Row(
            modifier = Modifier
                .clickable {
                    scaleExpanded = true
                    alignmentExpanded = false
                }
                .padding(horizontal = 4.dp, vertical = 16.dp)
        ){
            Text(
                ContentScales[contentScale] ?: "Unknown Content Scale",
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .defaultMinSize(minWidth = 128.dp)
            )
            Icon(
                imageVector = if (scaleExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
                contentDescription = null,
            )
            DropdownMenu(
                scaleExpanded,
                onDismissRequest = { scaleExpanded = false },
            ) {
                ContentScales.forEach { (cs, label) ->
                    DropdownMenuItem(
                        onClick = { onContentScaleChangedState(cs) },
                    ) {
                        Text(label)
                    }
                }
            }
        }
        Row(
            modifier = Modifier
                .clickable {
                    alignmentExpanded = true
                    scaleExpanded = false
                }
                .padding(horizontal = 4.dp, vertical = 16.dp)
        ) {
            Text(
                Alignments[alignment] ?: "Unknown Alignment",
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .defaultMinSize(minWidth = 128.dp)
            )
            Icon(
                imageVector = if (alignmentExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
                contentDescription = null,
            )
            DropdownMenu(
                alignmentExpanded,
                onDismissRequest = { alignmentExpanded = false }
            ) {
                Alignments.forEach { (a, label) ->
                    DropdownMenuItem(
                        onClick = { onAlignmentChangedState(a) },
                    ) {
                        Text(label)
                    }
                }
            }
        }
    }
}

private val Alignments = mapOf(
    Alignment.TopStart to "TopStart",
    Alignment.TopCenter to "TopCenter",
    Alignment.TopEnd to "TopEnd",
    Alignment.CenterStart to "CenterStart",
    Alignment.Center to "Center",
    Alignment.CenterEnd to "CenterEnd",
    Alignment.BottomStart to "BottomStart",
    Alignment.BottomCenter to "BottomCenter",
    Alignment.BottomEnd to "BottomEnd",
)

private val ContentScales = mapOf(
    ContentScale.Fit to "Fit",
    ContentScale.Crop to "Crop",
    ContentScale.Inside to "Inside",
    ContentScale.None to "None",
    ContentScale.FillBounds to "FillBounds",
    ContentScale.FillHeight to "FillHeight",
    ContentScale.FillWidth to "FillWidth",
)

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 + Blur", "Click to change color") {
                HeartColor()
            }
            ExampleCard("Change Properties", "Click to toggle whether the dynamic property is used") {
                ToggleProperty()
            }
            ExampleCard("Jump Height", "Click to jump height") {
                JumpHeight()
            }
        }
    }
}

@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 blurRadius = with(LocalDensity.current) { 12.dp.toPx() }

    val dynamicProperties = rememberLottieDynamicProperties(
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = color.toArgb(),
            keyPath = arrayOf(
                "H2",
                "Shape 1",
                "Fill 1",
            )
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.BLUR_RADIUS,
            value = blurRadius,
            keyPath = arrayOf(
                "**",
                "Stroke 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)
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun ExampleCard(
    name: String,
    description: String,
    content: @Composable () -> Unit,
) {
    Surface(
        shape = RoundedCornerShape(6.dp),
        elevation = 8.dp,
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
            .padding(bottom = 16.dp)
            .padding(horizontal = 48.dp)
    ) {
        Column {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .aspectRatio(1f)
                    .background(Color.Gray)
            ) {
                content()
            }
            Text(
                name,
                modifier = Modifier
                    .padding(horizontal = 16.dp)
                    .padding(top = 16.dp, bottom = 4.dp)
            )
            Text(
                description,
                color = Color.Gray,
                fontSize = 12.sp,
                modifier = Modifier
                    .padding(horizontal = 16.dp)
                    .padding(bottom = 16.dp)
            )
        }
    }
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ListItem
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.Route
import com.airbnb.lottie.sample.compose.composables.Marquee
import com.airbnb.lottie.sample.compose.navigate

@Composable
fun ExamplesPage(navController: NavController) {
    Column(
        modifier = Modifier
            .verticalScroll(rememberScrollState())
    ) {
        Marquee(stringResource(R.string.examples_title))
        ListItem(
            text = { Text("Basic Usage") },
            secondaryText = { Text("Various example of simple Lottie usage") },
            modifier = Modifier
                .clickable { navController.navigate(Route.BasicUsageExamples) }
        )
        ListItem(
            text = { Text("Animatable Usage") },
            secondaryText = { Text("Usage of LottieAnimatable") },
            modifier = Modifier
                .clickable { navController.navigate(Route.AnimatableUsageExamples) }
        )
        ListItem(
            text = { Text("Transitions") },
            secondaryText = { Text("Sequencing segments of an animation based on state") },
            modifier = Modifier
                .clickable { navController.navigate(Route.TransitionsExamples) }
        )
        ListItem(
            text = { Text("View Pager") },
            secondaryText = { Text("Syncing a Lottie animation with a view pager") },
            modifier = Modifier
                .clickable { navController.navigate(Route.ViewPagerExample) }
        )
        ListItem(
            text = { Text("Network Animations") },
            secondaryText = { Text("Loading animations from a url") },
            modifier = Modifier
                .clickable { navController.navigate(Route.NetworkExamples) }
        )
        ListItem(
            text = { Text("Dynamic Properties") },
            secondaryText = { Text("Setting dynamic properties") },
            modifier = Modifier
                .clickable { navController.navigate(Route.DynamicPropertiesExamples) }
        )
        ListItem(
            text = { Text("Images") },
            secondaryText = { Text("Using animations with images") },
            modifier = Modifier
                .clickable { navController.navigate(Route.ImagesExamples) }
        )
        ListItem(
            text = { Text("Text") },
            secondaryText = { Text("Using animations with text") },
            modifier = Modifier
                .clickable { navController.navigate(Route.TextExamples) }
        )
        ListItem(
            text = { Text("ContentScale and Alignment") },
            secondaryText = { Text("Changing an animation's ContentScale and Alignment") },
            modifier = Modifier
                .clickable { navController.navigate(Route.ContentScaleExamples) }
        )
        ListItem(
            text = { Text("Caching") },
            secondaryText = { Text("Interacting with Lottie's composition cache") },
            modifier = Modifier
                .clickable { navController.navigate(Route.CachingExamples) }
        )
    }
}

package com.airbnb.lottie.sample.compose.examples

import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.airbnb.lottie.sample.compose.R

@Composable
fun UsageExamplePageScaffold(
    content: @Composable (padding: PaddingValues) -> Unit,
) {
    val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
    Scaffold(
        topBar = {
            TopAppBar(
                title = {},
                navigationIcon = {
                    IconButton(
                        onClick = { backPressedDispatcher?.onBackPressed() },
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = stringResource(R.string.back)
                        )
                    }
                },
            )
        }
    ) { padding ->
        content(padding)
    }
}

package com.airbnb.lottie.sample.compose.examples

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
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.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.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

@Composable
fun ImagesExamplesPage() {
    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())
        ) {
            ExampleCard("Inline Image", "base64 image embedded in json file") {
                InlineImage()
            }
            ExampleCard("Assets Image", "Image stored in assets") {
                ImageAssets()
            }
            ExampleCard("Dynamic Properties", "Replace an image with dynamic properties") {
                DynamicProperties()
            }
            ExampleCard("Store on LottieImageAsset", "Store the bitmap within LottieImageAsset") {
                StoredOnImageAsset()
            }
        }
    }
}

@Composable
fun InlineImage() {
    // 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_inline_image),
        cacheKey = null,
    )
    LottieAnimation(
        composition,
        iterations = LottieConstants.IterateForever,
    )
}

@Composable
fun ImageAssets() {
    // 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,
        imageAssetsFolder = "Images/WeAccept",
    )
    LottieAnimation(
        composition,
        iterations = LottieConstants.IterateForever,

    )
}

@Composable
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 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(
        composition,
        iterations = LottieConstants.IterateForever,
    )
}

@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(asset)
        val opts = BitmapFactory.Options()
        opts.inScaled = true
        opts.inDensity = 1606
        BitmapFactory.decodeStream(inputSteam, null, opts)
    } catch (e: Exception) {
        null
    }
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.compose.rememberLottieRetrySignal

@Composable
fun NetworkExamplesPage() {
    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())
        ) {
            Box(modifier = Modifier.height(16.dp))
            ExampleCard("Example 1", "Basic URL") {
                Example1()
            }
            ExampleCard("Example 2", "Fail with retries. Click to retry.") {
                Example2()
            }
        }
    }
}

@Composable
private fun Example1() {
    val composition by rememberLottieComposition(LottieCompositionSpec.Url("https://raw.githubusercontent.com/airbnb/lottie-android/master/sample/src/main/res/raw/heart.json"))
    LottieAnimation(composition)
}

@Composable
private fun Example2() {
    val retrySignal = rememberLottieRetrySignal()
    var failedCount by remember { mutableStateOf(0) }
    val composition by rememberLottieComposition(
        LottieCompositionSpec.Url("not a url"),
        onRetry = { fc, _ ->
            failedCount = fc
            // Await the retry signal.
            retrySignal.awaitRetry()
            true
        }
    )
    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable { retrySignal.retry() }
    ) {
        LottieAnimation(composition)
        Text(
            "Failed $failedCount times.\nAwaiting retry: ${retrySignal.isAwaitingRetry}",
            color = Color.LightGray,
            modifier = Modifier
                .padding(8.dp)
                .align(Alignment.BottomStart)
        )
    }
}

package com.airbnb.lottie.sample.compose.examples

import android.graphics.Typeface
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.LaunchedEffect
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.LottieProperty
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
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

@Composable
fun TextExamplesPage() {
    UsageExamplePageScaffold {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())
        ) {
            ExampleCard("Default", "Loading fonts using default asset paths") {
                Example1()
            }
            ExampleCard("Dynamic Properties", "Replace fonts with custom typefaces") {
                Example2()
            }
        }
    }
}

@Composable
private fun Example1() {
    // Lottie will automatically look for fonts in src/main/assets/fonts.
    // It will find font files based on the font family specified in the Lottie Json file.
    // You can specify a different assets subfolder by using the fontAssetsFolder parameter.
    // By default, it will look for ttf files.
    // You can specify a different file extension by using the fontFileExtension parameter.
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.name))
    LottieAnimation(
        composition,
        progress = 0f,
    )
}

@Composable
private fun Example2() {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.name))
    val typeface = rememberTypeface("fonts/Roboto.ttf")
    val dynamicProperties = rememberLottieDynamicProperties(
        rememberLottieDynamicProperty(LottieProperty.TYPEFACE, typeface, "NAME")
    )

    LottieAnimation(
        composition,
        progress = 0f,
        dynamicProperties = dynamicProperties,
    )
}

@Composable
private fun rememberTypeface(path: String): Typeface? {
    var typeface: Typeface? by remember { mutableStateOf(null) }
    val context = LocalContext.current
    LaunchedEffect(path) {
        typeface = null
        withContext(Dispatchers.IO) {
            typeface = Typeface.createFromAsset(context.assets, "fonts/Roboto.ttf")
        }
    }
    return typeface
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.rememberLottieAnimatable
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCancellationBehavior
import com.airbnb.lottie.compose.LottieClipSpec
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R
import kotlinx.coroutines.flow.collectLatest

enum class TransitionSection {
    Intro,
    LoopMiddle,
    Outro;

    fun next(): TransitionSection = when (this) {
        Intro -> LoopMiddle
        LoopMiddle -> Outro
        Outro -> Intro
    }
}

@Composable
fun TransitionsExamplesPage() {
    var state by remember { mutableStateOf(TransitionSection.Intro) }

    UsageExamplePageScaffold { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
        ) {
            Text(
                "Single composition",
                modifier = Modifier
                    .padding(8.dp)
            )
            SingleCompositionTransition(state)
            Text(
                "Multiple compositions",
                modifier = Modifier
                    .padding(8.dp)
            )
            SplitCompositionTransition(state)
            TextButton(
                onClick = { state = state.next() }
            ) {
                Text("State: $state")
            }
        }

    }
}

@Composable
fun SingleCompositionTransition(section: TransitionSection) {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar))
    val animatable = rememberLottieAnimatable()
    val state by rememberUpdatedState(section)

    LaunchedEffect(composition, animatable) {
        composition ?: return@LaunchedEffect
        snapshotFlow { state }.collectLatest { s ->
            val clipSpec = when (s) {
                TransitionSection.Intro -> LottieClipSpec.Progress(0f, 0.301f)
                TransitionSection.LoopMiddle -> LottieClipSpec.Progress(0.301f, 2f / 3f)
                TransitionSection.Outro -> LottieClipSpec.Progress(2f / 3f, 1f)
            }
            do {
                animatable.animate(
                    composition,
                    clipSpec = clipSpec,
                    cancellationBehavior = LottieCancellationBehavior.OnIterationFinish,
                )
            } while (s == TransitionSection.LoopMiddle)
        }
    }
    LottieAnimation(composition, animatable.progress)
}

@Composable
fun SplitCompositionTransition(section: TransitionSection) {
    val introComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar_1))
    val loopMiddleComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar_2))
    val outroComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bar_3))
    val animatable = rememberLottieAnimatable()

    LaunchedEffect(section) {
        val composition = when (section) {
            TransitionSection.Intro -> introComposition
            TransitionSection.LoopMiddle -> loopMiddleComposition
            TransitionSection.Outro -> outroComposition
        }.await()
        animatable.animate(
            composition,
            iterations = if (section == TransitionSection.LoopMiddle) LottieConstants.IterateForever else 1,
            cancellationBehavior = LottieCancellationBehavior.OnIterationFinish,
        )
    }

    LottieAnimation(animatable.composition, animatable.progress)
}

package com.airbnb.lottie.sample.compose.examples

import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator
import com.google.accompanist.pager.PagerDefaults
import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState

@Composable
fun ViewPagerExamplePage() {
    val colors = listOf(Color.Red, Color.Green, Color.Blue, Color.Magenta)
    val pagerState = rememberPagerState(pageCount = colors.size)
    Box(
        modifier = Modifier
            .fillMaxSize()
    ) {
        HorizontalPager(
            pagerState,
            flingBehavior = PagerDefaults.rememberPagerFlingConfig(
                pagerState,
                decayAnimationSpec = exponentialDecay(frictionMultiplier = 0.05f)
            )
        ) { page ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(colors[page])
            )
        }
        WalkthroughAnimation(pagerState)
        HorizontalPagerIndicator(
            pagerState,
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 32.dp)
        )

    }
}

@Composable
private fun WalkthroughAnimation(pagerState: PagerState) {
    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.walkthrough))
    val progress by derivedStateOf { (pagerState.currentPage + pagerState.currentPageOffset) / (pagerState.pageCount - 1f) }
    LottieAnimation(
        composition,
        progress,
        modifier = Modifier
            .fillMaxSize()
    )
}

package com.airbnb.lottie.sample.compose.lottiefiles

import androidx.annotation.StringRes
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.composables.Marquee

enum class LottieFilesTab(@StringRes val stringRes: Int) {
    Recent(R.string.tab_recent),
    Popular(R.string.tab_popular),
    Search(R.string.tab_search)
}

@Composable
fun LottieFilesPage(navController: NavController) {
    var tab by rememberSaveable { mutableStateOf(LottieFilesTab.Recent) }

    Column {
        Marquee("LottieFiles")
        LottieFilesTabBar(
            selectedTab = tab,
            onTabSelected = { tab = it },
        )
        when (tab) {
            LottieFilesTab.Recent -> LottieFilesRecentAndPopularPage(navController, LottieFilesMode.Recent)
            LottieFilesTab.Popular -> LottieFilesRecentAndPopularPage(navController, LottieFilesMode.Popular)
            LottieFilesTab.Search -> LottieFilesSearchPage(navController)
        }
    }

}

@Composable
fun LottieFilesTabBar(
    selectedTab: LottieFilesTab,
    onTabSelected: (LottieFilesTab) -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        horizontalArrangement = Arrangement.Start,
        modifier = modifier
            .fillMaxWidth(),
    ) {
        for (tab in LottieFilesTab.values()) {
            LottieFilesTabBarTab(
                text = stringResource(tab.stringRes),
                isSelected = tab == selectedTab,
                onClick = { onTabSelected(tab) },
            )
        }
    }
}

@Composable
fun LottieFilesTabBarTab(
    text: String,
    isSelected: Boolean,
    onClick: () -> Unit
) {
    val textWidth = remember { mutableStateOf(0) }
    val pxRatio = with(LocalDensity.current) { 1.dp.toPx() }
    val tabWidth by animateDpAsState(if (isSelected) (textWidth.value / pxRatio).dp else 0.dp)
    val tabAlpha by animateFloatAsState(if (isSelected) 1f else 0f)
    Column(
        modifier = Modifier
            .clickable(onClick = onClick)
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Text(
            text,
            maxLines = 1,
            modifier = Modifier
                .onGloballyPositioned { textWidth.value = it.size.width }
                .wrapContentWidth()
        )
        Box(
            modifier = Modifier
                .align(Alignment.CenterHorizontally)
                .height(3.dp)
                .background(MaterialTheme.colors.primary.copy(alpha = tabAlpha))
                .width(tabWidth)
        )
    }
}

package com.airbnb.lottie.sample.compose.lottiefiles

import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.airbnb.lottie.sample.compose.Route
import com.airbnb.lottie.sample.compose.api.AnimationDataV2
import com.airbnb.lottie.sample.compose.api.LottieFilesApi
import com.airbnb.lottie.sample.compose.composables.AnimationRow
import com.airbnb.lottie.sample.compose.dagger.AssistedViewModelFactory
import com.airbnb.lottie.sample.compose.dagger.daggerMavericksViewModelFactory
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

enum class LottieFilesMode {
    Recent,
    Popular,
}

data class LottieFilesRecentAndPopularState(
    val mode: LottieFilesMode = LottieFilesMode.Recent,
    val results: List<AnimationDataV2> = emptyList(),
    val currentPage: Int = 1,
    val lastPage: Int = 0,
    val fetchException: Boolean = false,
) : MavericksState

class LottieFilesRecentAndPopularViewModel @AssistedInject constructor(
    @Assisted initialState: LottieFilesRecentAndPopularState,
    private val api: LottieFilesApi,
) : MavericksViewModel<LottieFilesRecentAndPopularState>(initialState) {
    private var fetchJob: Job? = null

    init {
        onEach(LottieFilesRecentAndPopularState::mode) {
            setState { copy(results = emptyList(), currentPage = 0, lastPage = 1, fetchException = false) }
            withState {
                fetchNextPage()
            }
        }
    }

    fun fetchNextPage() = withState { state ->
        fetchJob?.cancel()
        if (state.currentPage >= state.lastPage) return@withState
        fetchJob = viewModelScope.launch {
            val response = try {
                Log.d(TAG, "Fetching page ${state.currentPage + 1}")
                when (state.mode) {
                    LottieFilesMode.Recent -> api.getRecent(state.currentPage + 1)
                    LottieFilesMode.Popular -> api.getPopular(state.currentPage + 1)
                }
            } catch (e: Exception) {
                Log.w(TAG, "Failed to fetch from Lottie Files.", e)
                setState { copy(fetchException = true) }
                return@launch
            }
            setState {
                copy(
                    results = results + response.data.map(::AnimationDataV2),
                    currentPage = response.current_page,
                    lastPage = response.last_page,
                    fetchException = false
                )
            }
        }
    }

    fun setMode(mode: LottieFilesMode) = setState { copy(mode = mode) }

    @AssistedFactory
    interface Factory : AssistedViewModelFactory<LottieFilesRecentAndPopularViewModel, LottieFilesRecentAndPopularState> {
        override fun create(initialState: LottieFilesRecentAndPopularState): LottieFilesRecentAndPopularViewModel
    }

    companion object :
        MavericksViewModelFactory<LottieFilesRecentAndPopularViewModel, LottieFilesRecentAndPopularState> by daggerMavericksViewModelFactory() {
        private const val TAG = "LottieFilesVM"
    }
}

@Composable
fun LottieFilesRecentAndPopularPage(navController: NavController, mode: LottieFilesMode) {
    val viewModel: LottieFilesRecentAndPopularViewModel = mavericksViewModel()
    val state by viewModel.collectAsState()
    SideEffect {
        viewModel.setMode(mode)
    }
    LottieFilesRecentAndPopularPage(
        state,
        viewModel::fetchNextPage,
        onAnimationClicked = { data ->
            navController.navigate(Route.Player.forUrl(data.file, backgroundColor = data.bg_color))
        }
    )
}

@Composable
fun LottieFilesRecentAndPopularPage(
    state: LottieFilesRecentAndPopularState,
    fetchNextPage: () -> Unit,
    onAnimationClicked: (AnimationDataV2) -> Unit,
    modifier: Modifier = Modifier,
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
    ) {
        Column(
            modifier = Modifier.then(modifier)
        ) {
            LazyColumn(
                modifier = Modifier.weight(1f)
            ) {
                itemsIndexed(state.results) { index, result ->
                    if (index == state.results.size - 1) {
                        SideEffect(fetchNextPage)
                    }
                    AnimationRow(
                        title = result.title,
                        previewUrl = result.preview_url ?: "",
                        previewBackgroundColor = result.bgColor,
                        onClick = { onAnimationClicked(result) }
                    )
                }
            }
        }
        if (state.fetchException) {
            FloatingActionButton(
                onClick = fetchNextPage,
                content = {
                    Icon(
                        imageVector = Icons.Filled.Repeat,
                        tint = Color.White,
                        contentDescription = null
                    )
                },
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = 24.dp)
            )
        }
    }
}

package com.airbnb.lottie.sample.compose.lottiefiles

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.Route
import com.airbnb.lottie.sample.compose.api.AnimationDataV2
import com.airbnb.lottie.sample.compose.api.LottieFilesApi
import com.airbnb.lottie.sample.compose.composables.AnimationRow
import com.airbnb.lottie.sample.compose.dagger.AssistedViewModelFactory
import com.airbnb.lottie.sample.compose.dagger.daggerMavericksViewModelFactory
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

data class LottieFilesSearchState(
    val query: String = "",
    val results: List<AnimationDataV2> = emptyList(),
    val currentPage: Int = 1,
    val lastPage: Int = 0,
    val fetchException: Boolean = false,
) : MavericksState

class LottieFilesSearchViewModel @AssistedInject constructor(
    @Assisted initialState: LottieFilesSearchState,
    private val api: LottieFilesApi,
) : MavericksViewModel<LottieFilesSearchState>(initialState) {
    private var fetchJob: Job? = null

    init {
        onEach(LottieFilesSearchState::query) { query ->
            fetchJob?.cancel()
            if (query.isBlank()) {
                setState { copy(results = emptyList(), currentPage = 1, lastPage = 1, fetchException = false) }
            } else {
                fetchJob = viewModelScope.launch {
                    val results = try {
                        api.search(query, 1)
                    } catch (e: Exception) {
                        setState { copy(fetchException = true) }
                        return@launch
                    }
                    setState {
                        copy(
                            results = results.data.map(::AnimationDataV2),
                            currentPage = results.current_page,
                            lastPage = results.last_page,
                            fetchException = false
                        )
                    }
                }
            }
        }
    }

    fun fetchNextPage() = withState { state ->
        fetchJob?.cancel()
        if (state.currentPage >= state.lastPage) return@withState
        fetchJob = viewModelScope.launch {
            val response = try {
                api.search(state.query, state.currentPage + 1)
            } catch (e: Exception) {
                setState { copy(fetchException = true) }
                return@launch
            }
            setState {
                copy(
                    results = results + response.data.map(::AnimationDataV2),
                    currentPage = response.current_page,
                    fetchException = false
                )
            }
        }
    }

    fun setQuery(query: String) = setState { copy(query = query, currentPage = 1, results = emptyList()) }

    @AssistedFactory
    interface Factory : AssistedViewModelFactory<LottieFilesSearchViewModel, LottieFilesSearchState> {
        override fun create(initialState: LottieFilesSearchState): LottieFilesSearchViewModel
    }

    companion object : MavericksViewModelFactory<LottieFilesSearchViewModel, LottieFilesSearchState> by daggerMavericksViewModelFactory()
}

@Composable
fun LottieFilesSearchPage(navController: NavController) {
    val viewModel: LottieFilesSearchViewModel = mavericksViewModel()
    val state by viewModel.collectAsState()
    LottieFilesSearchPage(
        state,
        viewModel::setQuery,
        viewModel::fetchNextPage,
        onAnimationClicked = { data ->
            navController.navigate(Route.Player.forUrl(data.file, backgroundColor = data.bg_color))
        }
    )
}

@Composable
fun LottieFilesSearchPage(
    state: LottieFilesSearchState,
    onQueryChanged: (String) -> Unit,
    fetchNextPage: () -> Unit,
    onAnimationClicked: (AnimationDataV2) -> Unit,
    modifier: Modifier = Modifier,
) {
    Box {
        Column(
            modifier = Modifier.then(modifier)
        ) {
            OutlinedTextField(
                value = state.query,
                onValueChange = onQueryChanged,
                label = { Text(stringResource(R.string.query)) },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 16.dp),
                singleLine = true
            )
            LazyColumn(
                modifier = Modifier.weight(1f)
            ) {
                itemsIndexed(state.results) { index, result ->
                    if (index == state.results.size - 1) {
                        SideEffect(fetchNextPage)
                    }
                    AnimationRow(
                        title = result.title,
                        previewUrl = result.preview_url ?: "",
                        previewBackgroundColor = result.bgColor,
                        onClick = { onAnimationClicked(result) }
                    )
                }
            }
        }
        if (state.fetchException) {
            FloatingActionButton(
                onClick = fetchNextPage,
                content = {
                    Icon(
                        imageVector = Icons.Filled.Repeat,
                        tint = Color.White,
                        contentDescription = null
                    )
                },
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = 24.dp)
            )
        }
    }
}

@Preview
@Composable
fun PreviewSearchPage() {
    val data = AnimationDataV2(0, null, "https://assets9.lottiefiles.com/render/k1821vf5.png", "Loading", "")
    val state = LottieFilesSearchState(
        results = listOf(data, data, data),
        fetchException = true
    )
    Surface(color = Color.White) {
        LottieFilesSearchPage(
            state = state,
            onQueryChanged = {},
            fetchNextPage = {},
            onAnimationClicked = {}
        )
    }
}

package com.airbnb.lottie.sample.compose.player

import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Slider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.RemoveRedEye
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.compose.LottieAnimatable
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.resetToBeginning
import com.airbnb.lottie.sample.compose.BuildConfig
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.composables.DebouncedCircularProgressIndicator
import com.airbnb.lottie.sample.compose.ui.Teal
import com.airbnb.lottie.sample.compose.utils.drawBottomBorder
import com.airbnb.lottie.sample.compose.utils.maybeBackground
import com.airbnb.lottie.sample.compose.utils.maybeDrawBorder
import com.airbnb.lottie.sample.compose.utils.toDummyBitmap
import kotlin.math.ceil
import kotlin.math.roundToInt

@Stable
class PlayerPageState(backgroundColor: Color?) {
    val animatable = LottieAnimatable()

    var backgroundColor by mutableStateOf(backgroundColor)
    var outlineMasksAndMattes by mutableStateOf(false)
    var applyOpacityToLayers by mutableStateOf(false)
    var enableMergePaths by mutableStateOf(false)
    var focusMode by mutableStateOf(false)
    var showWarningsDialog by mutableStateOf(false)

    var borderToolbar by mutableStateOf(false)
    var speedToolbar by mutableStateOf(false)
    var backgroundColorToolbar by mutableStateOf(false)

    var progressSliderGesture: Float? by mutableStateOf(null)
    var shouldPlay by mutableStateOf(true)
    var targetSpeed by mutableStateOf(1f)
    var shouldLoop by mutableStateOf(true)
}

@Composable
fun PlayerPage(
    spec: LottieCompositionSpec,
    animationBackgroundColor: Color? = null,
) {
    val scaffoldState = rememberScaffoldState()
    val state = remember { PlayerPageState(animationBackgroundColor) }

    val failedMessage = stringResource(R.string.failed_to_load)
    val okMessage = stringResource(R.string.ok)

    val compositionResult = rememberLottieComposition(spec)

    LaunchedEffect(compositionResult.isFailure) {
        if (!compositionResult.isFailure) return@LaunchedEffect
        scaffoldState.snackbarHostState.showSnackbar(
            message = failedMessage,
            actionLabel = okMessage,
        )
    }

    val dummyBitmapStrokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
    LaunchedEffect(compositionResult.value) {
        val composition = compositionResult.value ?: return@LaunchedEffect
        for (asset in composition.images.values) {
            if (asset.bitmap != null) continue
            asset.bitmap = asset.toDummyBitmap(dummyBitmapStrokeWidth)
        }
    }

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = { PlayerPageTopAppBar(state, compositionResult.value) },
    ) {
        PlayerPageContent(
            state,
            compositionResult.value,
            compositionResult.isLoading,
            animationBackgroundColor,
        )
    }

    if (state.showWarningsDialog) {
        WarningDialog(warnings = compositionResult.value?.warnings ?: emptyList(), onDismiss = { state.showWarningsDialog = false })
    }
}

@Composable
private fun ColumnScope.ExpandVisibility(
    visible: Boolean,
    content: @Composable () -> Unit,
) {
    AnimatedVisibility(
        visible = visible,
        enter = expandVertically(),
        exit = shrinkVertically()
    ) {
        content()
    }
}

@Composable
private fun PlayerPageTopAppBar(
    state: PlayerPageState,
    composition: LottieComposition?,
) {
    val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher

    TopAppBar(
        title = {},
        backgroundColor = Color.Transparent,
        elevation = 0.dp,
        navigationIcon = {
            IconButton(
                onClick = { backPressedDispatcher?.onBackPressed() },
            ) {
                Icon(
                    Icons.Default.Close,
                    contentDescription = null
                )
            }
        },
        actions = {
            if (composition?.warnings?.isNotEmpty() == true) {
                IconButton(
                    onClick = { state.showWarningsDialog = true }
                ) {
                    Icon(
                        Icons.Filled.Warning,
                        tint = Color.Black,
                        contentDescription = null
                    )
                }
            }
            IconButton(
                onClick = { state.focusMode = !state.focusMode },
            ) {
                Icon(
                    Icons.Filled.RemoveRedEye,
                    tint = if (state.focusMode) Teal else Color.Black,
                    contentDescription = null
                )
            }
        }
    )
}

@Composable
fun PlayerPageContent(
    state: PlayerPageState,
    composition: LottieComposition?,
    isLoading: Boolean,
    animationBackgroundColor: Color?,
) {
    LaunchedEffect(
        composition,
        state.shouldPlay,
        state.targetSpeed,
        state.shouldLoop,
        state.progressSliderGesture,
    ) {
        composition ?: return@LaunchedEffect
        state.progressSliderGesture?.let { p ->
            state.animatable.snapTo(composition, p, resetLastFrameNanos = true)
            return@LaunchedEffect
        }
        if (state.shouldPlay) {
            if (!state.animatable.isPlaying && state.animatable.isAtEnd) {
                state.animatable.resetToBeginning()
            }
            state.animatable.animate(
                composition,
                iterations = if (state.shouldLoop) LottieConstants.IterateForever else 1,
                initialProgress = state.animatable.progress,
                speed = state.targetSpeed,
                continueFromPreviousAnimate = state.animatable.isPlaying,
            )
            state.shouldPlay = false
        }
    }

    Column(
        verticalArrangement = Arrangement.SpaceBetween,
        modifier = Modifier.fillMaxHeight()
    ) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier
                .weight(1f)
                .maybeBackground(state.backgroundColor)
                .fillMaxWidth()
        ) {
            PlayerPageLottieAnimation(
                composition,
                state.animatable.progress,
                modifier = Modifier
                    // TODO: figure out how maxWidth can play nice with the aspectRatio modifier inside of LottieAnimation.
                    .fillMaxWidth()
                    .align(Alignment.Center)
                    .maybeDrawBorder(state.borderToolbar)
            )
            if (isLoading) {
                DebouncedCircularProgressIndicator(
                    color = Teal,
                    modifier = Modifier
                        .size(48.dp)
                )
            }
        }
        ExpandVisibility(state.speedToolbar && !state.focusMode) {
            SpeedToolbar(state)
        }
        ExpandVisibility(!state.focusMode && state.backgroundColorToolbar) {
            BackgroundColorToolbar(
                animationBackgroundColor = animationBackgroundColor,
                onColorChanged = { state.backgroundColor = it }
            )
        }
        ExpandVisibility(!state.focusMode) {
            PlayerControlsRow(state, composition)
        }
        ExpandVisibility(!state.focusMode) {
            Toolbar(state)
        }
    }
}

@Composable
private fun PlayerPageLottieAnimation(
    composition: LottieComposition?,
    progress: Float,
    modifier: Modifier = Modifier,
) {
    LottieAnimation(
        composition,
        progress,
        modifier = modifier,
    )
}

@Composable
private fun PlayerControlsRow(
    state: PlayerPageState,
    composition: LottieComposition?,
) {
    val totalTime = ((composition?.duration ?: 0L / state.animatable.speed) / 1000.0)
    val totalTimeFormatted = ("%.1f").format(totalTime)

    val progressFormatted = ("%.1f").format(state.animatable.progress * totalTime)

    val frame = composition?.getFrameForProgress(state.animatable.progress)?.roundToInt() ?: 0
    val durationFrames = ceil(composition?.durationFrames ?: 0f).roundToInt()
    Box(
        modifier = Modifier
            .fillMaxWidth()
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Box(
                contentAlignment = Alignment.Center
            ) {
                IconButton(
                    onClick = { state.shouldPlay = !state.shouldPlay },
                ) {
                    Icon(
                        if (state.animatable.isPlaying) Icons.Filled.Pause
                        else Icons.Filled.PlayArrow,
                        contentDescription = null
                    )
                }
                Text(
                    "$frame/$durationFrames\n${progressFormatted}/$totalTimeFormatted",
                    style = TextStyle(fontSize = 8.sp),
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .padding(top = 48.dp, bottom = 8.dp)
                )
            }
            Slider(
                value = state.progressSliderGesture ?: state.animatable.progress,
                onValueChange = { state.progressSliderGesture = it },
                onValueChangeFinished = { state.progressSliderGesture = null },
                modifier = Modifier.weight(1f)
            )
            IconButton(
                onClick = { state.shouldLoop = !state.shouldLoop },
            ) {
                Icon(
                    Icons.Filled.Repeat,
                    tint = if (state.animatable.iterations == 1) Color.Black else Teal,
                    contentDescription = null
                )
            }
        }
        Text(
            BuildConfig.VERSION_NAME,
            fontSize = 6.sp,
            color = Color.Gray,
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 12.dp)
        )
    }
}

@Composable
private fun SpeedToolbar(state: PlayerPageState) {
    Row(
        horizontalArrangement = Arrangement.SpaceBetween,
        modifier = Modifier
            .drawBottomBorder()
            .padding(vertical = 12.dp, horizontal = 16.dp)
            .fillMaxWidth()
    ) {
        ToolbarChip(
            label = "0.5x",
            isActivated = state.animatable.speed == 0.5f,
            onClick = { state.targetSpeed = 0.5f },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            label = "1x",
            isActivated = state.animatable.speed == 1f,
            onClick = { state.targetSpeed = 1f },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            label = "1.5x",
            isActivated = state.animatable.speed == 1.5f,
            onClick = { state.targetSpeed = 1.5f },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            label = "2x",
            isActivated = state.animatable.speed == 2f,
            onClick = { state.targetSpeed = 2f },
            modifier = Modifier.padding(end = 8.dp)
        )
    }
}

@Composable
private fun BackgroundColorToolbar(
    animationBackgroundColor: Color?,
    onColorChanged: (Color) -> Unit,
) {
    Row(
        horizontalArrangement = Arrangement.SpaceBetween,
        modifier = Modifier
            .drawBottomBorder()
            .padding(vertical = 12.dp, horizontal = 16.dp)
            .fillMaxWidth()
    ) {
        listOfNotNull(
            colorResource(R.color.background_color1),
            colorResource(R.color.background_color2),
            colorResource(R.color.background_color3),
            colorResource(R.color.background_color4),
            colorResource(R.color.background_color5),
            colorResource(R.color.background_color6),
            animationBackgroundColor.takeIf { it != Color.White },
        ).forEachIndexed { i, color ->
            val strokeColor = if (i == 0) colorResource(R.color.background_color1_stroke) else color
            BackgroundToolbarItem(
                color = color,
                strokeColor = strokeColor,
                onClick = { onColorChanged(color) }
            )
        }
    }
}

@Composable
private fun BackgroundToolbarItem(
    color: Color,
    strokeColor: Color = color,
    onClick: () -> Unit,
) {
    Box(
        modifier = Modifier
            .clip(CircleShape)
            .background(color)
            .clickable(onClick = onClick)
            .size(24.dp)
            .border(1.dp, strokeColor, shape = CircleShape)
    )
}

@Composable
private fun Toolbar(state: PlayerPageState) {
    Row(
        modifier = Modifier
            .horizontalScroll(rememberScrollState())
            .padding(horizontal = 8.dp)
            .padding(bottom = 8.dp)
    ) {
        ToolbarChip(
            iconPainter = painterResource(R.drawable.ic_masks_and_mattes),
            label = stringResource(R.string.toolbar_item_masks),
            isActivated = state.outlineMasksAndMattes,
            onClick = { state.outlineMasksAndMattes = it },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            iconPainter = painterResource(R.drawable.ic_layers),
            label = stringResource(R.string.toolbar_item_opacity_layers),
            isActivated = state.applyOpacityToLayers,
            onClick = { state.applyOpacityToLayers = it },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            iconPainter = painterResource(R.drawable.ic_color),
            label = stringResource(R.string.toolbar_item_color),
            isActivated = state.backgroundColorToolbar,
            onClick = { state.backgroundColorToolbar = it },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            iconPainter = painterResource(R.drawable.ic_speed),
            label = stringResource(R.string.toolbar_item_speed),
            isActivated = state.speedToolbar,
            onClick = { state.speedToolbar = it },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            iconPainter = painterResource(R.drawable.ic_border),
            label = stringResource(R.string.toolbar_item_border),
            isActivated = state.borderToolbar,
            onClick = { state.borderToolbar = it },
            modifier = Modifier.padding(end = 8.dp)
        )
        ToolbarChip(
            iconPainter = rememberVectorPainter(Icons.Default.MergeType),
            label = stringResource(R.string.toolbar_item_merge_paths),
            isActivated = state.enableMergePaths,
            onClick = { state.enableMergePaths = it },
            modifier = Modifier.padding(end = 8.dp)
        )
    }
}

@Composable
fun WarningDialog(
    warnings: List<String>,
    onDismiss: () -> Unit,
) {
    Dialog(onDismissRequest = onDismiss) {
        Surface(
            shape = RoundedCornerShape(4.dp),
            modifier = Modifier
                .width(400.dp)
                .heightIn(min = 32.dp, max = 500.dp)
        ) {
            Box(
                contentAlignment = Alignment.TopCenter,
                modifier = Modifier
            ) {
                LazyColumn {
                    itemsIndexed(warnings) { i, warning ->
                        Text(
                            warning,
                            fontSize = 8.sp,
                            textAlign = TextAlign.Left,
                            modifier = Modifier
                                .fillMaxWidth()
                                .run { if (i != 0) drawBottomBorder() else this }
                                .padding(vertical = 12.dp, horizontal = 16.dp)
                        )
                    }
                }
            }
        }
    }
}

@Preview
@Composable
fun SpeedToolbarPreview() {
    val state = remember { PlayerPageState(null) }
    SpeedToolbar(state)
}

@Preview(name = "Player")
@Composable
fun PlayerPagePreview() {
    PlayerPage(LottieCompositionSpec.Url("https://lottiefiles.com/download/public/32922"))
}

package com.airbnb.lottie.sample.compose.player

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.ui.Teal

@Composable
fun ToolbarChip(
    label: String,
    isActivated: Boolean,
    onClick: (isActivated: Boolean) -> Unit,
    modifier: Modifier = Modifier,
    iconPainter: Painter? = null,
) {
    val unActivatedColor = remember { Color(0xFF444444) }
    Surface(
        onClick = { onClick(!isActivated) },
        shape = RoundedCornerShape(3.dp),
        color = if (isActivated) Teal else Color(0xFFEAEAEA),
        modifier = Modifier
            .then(modifier)
            .clipToBounds()
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .padding(horizontal = 8.dp, vertical = 4.dp)
        ) {
            if (iconPainter != null) {
                Icon(
                    iconPainter,
                    tint = if (isActivated) Color.White else unActivatedColor,
                    modifier = Modifier
                        .size(12.dp),
                    contentDescription = null
                )
                Spacer(modifier = Modifier.width(6.dp))
            }
            Text(
                label,
                color = if (isActivated) Color.White else unActivatedColor,
                style = TextStyle(fontSize = 12.sp)
            )
        }
    }
}

@Preview
@Composable
fun PreviewToolbarChip() {
    ToolbarChip(
        iconPainter = painterResource(R.drawable.ic_border),
        label = stringResource(R.string.toolbar_item_border),
        isActivated = false,
        onClick = {}
    )
}

package com.airbnb.lottie.sample.compose.preview

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.Route
import com.airbnb.lottie.sample.compose.composables.Marquee
import com.airbnb.lottie.sample.compose.ui.LottieTheme

@Composable
fun PreviewPage(navController: NavController) {
    var showingAssetsDialog by remember { mutableStateOf(false) }
    var showingUrlDialog by remember { mutableStateOf(false) }

    Column {
        Marquee(stringResource(R.string.tab_preview))
        PreviewRow(R.drawable.ic_qr_scan, R.string.scan_qr_code) {

        }
        PreviewRow(R.drawable.ic_file, R.string.open_file) {

        }
        PreviewRow(R.drawable.ic_network, R.string.enter_url) {
            showingUrlDialog = true
        }
        PreviewRow(R.drawable.ic_storage, R.string.load_from_assets) {
            showingAssetsDialog = true
        }
    }

    AssetsDialog(
        showingAssetsDialog,
        onDismiss = { showingAssetsDialog = false },
        onAssetSelected = { assetName ->
            navController.navigate(Route.Player.forAsset(assetName))
        }
    )
    UrlDialog(
        showingUrlDialog,
        onDismiss = { showingUrlDialog = false },
        onUrlSelected = { url ->
            navController.navigate(Route.Player.forUrl(url))
        }
    )
}

@Composable
private fun PreviewRow(
    @DrawableRes iconRes: Int,
    @StringRes textRes: Int,
    onClick: () -> Unit,
) {
    Surface(
        onClick = onClick,
    ) {
        Column {
            Row(
                modifier = Modifier
                    .height(48.dp)
            ) {
                Icon(
                    painterResource(iconRes),
                    modifier = Modifier
                        .align(Alignment.CenterVertically)
                        .padding(16.dp),
                    contentDescription = null
                )
                Text(
                    stringResource(textRes),
                    modifier = Modifier
                        .align(Alignment.CenterVertically)
                )
            }
            Divider(color = Color.LightGray)
        }
    }
}

@Composable
fun AssetsDialog(isShowing: Boolean, onDismiss: () -> Unit, onAssetSelected: (assetName: String) -> Unit) {
    if (!isShowing) return
    val context = LocalContext.current
    val assets = context.assets.list("")
        ?.asSequence()
        ?.filter { it.endsWith(".json") || it.endsWith(".zip") }
        ?.toList()
        ?: emptyList()
    Dialog(onDismissRequest = onDismiss) {
        Surface(
            shape = RoundedCornerShape(4.dp),
        ) {
            Column(
                modifier = Modifier
                    .padding(horizontal = 12.dp)
            ) {
                assets.forEach { asset ->
                    AssetRow(asset, onClick = {
                        onDismiss()
                        onAssetSelected(asset)
                    })
                }
            }
        }
    }
}

@Composable
fun UrlDialog(isShowing: Boolean, onDismiss: () -> Unit, onUrlSelected: (url: String) -> Unit) {
    if (!isShowing) return
    var url by remember { mutableStateOf("") }
    Dialog(onDismissRequest = {
        url = ""
        onDismiss()
    }) {
        Surface(
            shape = RoundedCornerShape(4.dp),
        ) {
            Column(
                modifier = Modifier.padding(16.dp)
            ) {
                Text(
                    stringResource(R.string.enter_url),
                    fontSize = 18.sp,
                )
                OutlinedTextField(
                    value = url,
                    onValueChange = { url = it },
                    label = { Text(stringResource(R.string.url)) },
                )
                TextButton(
                    onClick = { onUrlSelected(url) },
                    modifier = Modifier.align(Alignment.End)
                ) {
                    Text(stringResource(R.string.ok))
                }
            }
        }
    }
}

@Composable
private fun AssetRow(name: String, onClick: () -> Unit) {
    Surface(
        onClick = onClick,
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 12.dp)
    ) {
        Text(name)
    }
}

@Preview
@Composable
fun PreviewPagePreview() {
    val navController = rememberNavController()
    LottieTheme {
        Box(modifier = Modifier.background(Color.White)) {
            PreviewPage(navController)
        }
    }
}

package com.airbnb.lottie.sample.compose.showcase

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.airbnb.lottie.sample.compose.Route
import com.airbnb.lottie.sample.compose.composables.AnimationRow
import com.airbnb.lottie.sample.compose.composables.Loader
import com.airbnb.lottie.sample.compose.composables.Marquee
import com.airbnb.lottie.sample.compose.ui.LottieTheme
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel

@Composable
fun ShowcasePage(navController: NavController) {
    val viewModel: ShowcaseViewModel = mavericksViewModel()
    val featuredAnimations by viewModel.collectAsState(ShowcaseState::animations)
    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        LazyColumn {
            item {
                Marquee("Showcase")
            }
            items(featuredAnimations()?.data.orEmpty()) { data ->
                AnimationRow(
                    title = data.title,
                    previewUrl = data.preview_url ?: "",
                    previewBackgroundColor = data.bgColor,
                ) {
                    navController.navigate(Route.Player.forUrl(data.file, backgroundColor = data.bg_color))
                }
                Divider(color = Color.LightGray)
            }
        }
        if (featuredAnimations is Uninitialized || featuredAnimations is Loading) {
            Loader(modifier = Modifier.align(Alignment.Center))
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    val navController = rememberNavController()
    LottieTheme {
        ShowcasePage(navController)
    }
}

package com.airbnb.lottie.sample.compose.showcase

import com.airbnb.lottie.sample.compose.api.AnimationsResponseV2
import com.airbnb.lottie.sample.compose.api.LottieFilesApi
import com.airbnb.lottie.sample.compose.dagger.AssistedViewModelFactory
import com.airbnb.lottie.sample.compose.dagger.daggerMavericksViewModelFactory
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

data class ShowcaseState(
    val animations: Async<AnimationsResponseV2> = Uninitialized
) : MavericksState

class ShowcaseViewModel @AssistedInject constructor(
        @Assisted initialState: ShowcaseState,
        private var api: LottieFilesApi
) : MavericksViewModel<ShowcaseState>(initialState) {

    init {
        fetchFeatured()
    }

    private fun fetchFeatured() {
        suspend {
            api.getFeatured()
        }.execute { copy(animations = it) }
    }

    @AssistedFactory
    interface Factory : AssistedViewModelFactory<ShowcaseViewModel, ShowcaseState> {
        override fun create(initialState: ShowcaseState): ShowcaseViewModel
    }

    companion object : MavericksViewModelFactory<ShowcaseViewModel, ShowcaseState> by daggerMavericksViewModelFactory()
}

package com.airbnb.lottie.sample.compose.ui

import android.util.Log
import androidx.annotation.ColorInt
import androidx.compose.ui.graphics.Color
import androidx.core.graphics.toColorInt
import com.airbnb.lottie.L

val purple200 = Color(0xFFBB86FC)
val TealDark = Color(0xFF484848)
val Teal = Color(0xFF009587)

@ColorInt
fun String?.toColorSafe(): Color {
    var bgColor = this ?: "#ffffff"
    bgColor = if (bgColor.startsWith("#")) bgColor else "#$bgColor"

    val colorInt = try {
        when (bgColor.length) {
            0 -> "#ffffff"
            4 -> "#%c%c%c%c%c%c".format(
                bgColor[1], bgColor[1],
                bgColor[2], bgColor[2],
                bgColor[3], bgColor[3]
            )
            5 -> "#%c%c%c%c%c%c%c%c".format(
                bgColor[1], bgColor[1],
                bgColor[2], bgColor[2],
                bgColor[3], bgColor[3],
                bgColor[4], bgColor[4]
            )
            else -> bgColor
        }.toColorInt()
    } catch (e: IllegalArgumentException) {
        Log.w(L.TAG, "Unable to parse $bgColor.")
        android.graphics.Color.WHITE
    }
    return Color(colorInt)
}

package com.airbnb.lottie.sample.compose.ui

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp

val shapes = Shapes(
        small = RoundedCornerShape(4.dp),
        medium = RoundedCornerShape(4.dp),
        large = RoundedCornerShape(0.dp)
)

package com.airbnb.lottie.sample.compose.ui

import androidx.compose.material.MaterialTheme
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable

private val ColorPalette = lightColors(
        primary = Teal,
        primaryVariant = TealDark,
        secondary = purple200
)

@Composable
fun LottieTheme(content: @Composable () -> Unit) {
    MaterialTheme(
            colors = ColorPalette,
            typography = typography,
            shapes = shapes,
            content = content
    )
}

package com.airbnb.lottie.sample.compose.ui

import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// Set of Material typography styles to start with
val typography = Typography(
        body1 = TextStyle(
                fontFamily = FontFamily.Default,
                fontWeight = FontWeight.Normal,
                fontSize = 16.sp
        )
)

package com.airbnb.lottie.sample.compose.utils

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import com.airbnb.lottie.LottieImageAsset

fun LottieImageAsset.toDummyBitmap(strokeWidth: Float): Bitmap {
    val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    val paint = Paint()
    paint.color = Color.GRAY
    paint.style = Paint.Style.STROKE
    paint.strokeWidth = strokeWidth
    canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
    canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), paint)
    canvas.drawLine(width.toFloat(), 0f, 0f, height.toFloat(), paint)
    return bitmap
}

package com.airbnb.lottie.sample.compose.utils

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

fun Modifier.maybeBackground(color: Color?): Modifier {
    return if (color == null) {
        this
    } else {
        this.then(background(color))
    }
}

fun Modifier.drawTopBorder(color: Color = Color.DarkGray) = this.then(drawBehind {
    drawRect(color, Offset.Zero, size = Size(size.width, 1f))
})

fun Modifier.drawBottomBorder(color: Color = Color.DarkGray) = this.then(drawBehind {
    drawRect(color, Offset(0f, size.height - 1f), size = Size(size.width, 1f))
})

fun Modifier.maybeDrawBorder(draw: Boolean, color: Color = Color.Black, width: Dp = 1.dp): Modifier {
    return if (draw) {
        this.then(border(width, color))
    } else {
        this
    }
}

package com.airbnb.lottie.sample.compose.utils

import android.os.Bundle
import android.util.Base64
import java.nio.charset.StandardCharsets

fun Bundle.getBase64String(key: String) = String(Base64.decode(getString(key), Base64.DEFAULT), StandardCharsets.UTF_8)