| package com.airbnb.lottie.compose |
| |
| import androidx.compose.animation.core.AnimationConstants |
| import com.airbnb.lottie.LottieComposition |
| import com.airbnb.lottie.LottieCompositionFactory |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.cancelAndJoin |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.test.runBlockingTest |
| import kotlinx.coroutines.withContext |
| import org.junit.Assert.* |
| import org.junit.Before |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.robolectric.RobolectricTestRunner |
| |
| @RunWith(RobolectricTestRunner::class) |
| class LottieAnimatableImplTest { |
| |
| private lateinit var clock: TestFrameClock |
| private lateinit var anim: LottieAnimatable |
| private lateinit var composition: LottieComposition |
| private val compositionDuration get() = composition.duration.toLong() |
| |
| @Before |
| fun setup() { |
| clock = TestFrameClock() |
| anim = LottieAnimatable() |
| composition = LottieCompositionFactory.fromJsonStringSync(CompositionFixtures.Rect, null).value!! |
| } |
| |
| @Test |
| fun testSingleIterationProgress() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertEquals(0f, anim.progress) |
| clock.frameMs(0) |
| clock.frameMs(300) |
| assertEquals(0.5f, anim.progress, 0.01f) |
| clock.frameMs(composition.duration.toLong() - 1) |
| assertFalse(anim.isAtEnd) |
| assertTrue(anim.isPlaying) |
| clock.frameMs(composition.duration.toLong()) |
| assertTrue(anim.isAtEnd) |
| assertFalse(anim.isPlaying) |
| } |
| |
| @Test |
| fun testJumpFromOneIterationToEndOfNext() = runTest { |
| launch { |
| anim.animate(composition, iterations = 2) |
| } |
| assertEquals(0f, anim.progress) |
| clock.frameMs(0) |
| |
| assertFrame(300, progress = 0.5f, iterations = 2) |
| assertFrame(compositionDuration - 1, progress = 0.998f, iteration = 1, iterations = 2) |
| assertFrame(2 * compositionDuration, progress = 1f, iteration = 2, iterations = 2, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testTwoIterations() = runTest { |
| launch { |
| anim.animate(composition, iterations = 2) |
| } |
| assertEquals(0f, anim.progress) |
| clock.frameMs(0) |
| |
| assertFrame(300, progress = 0.5f, iterations = 2) |
| assertFrame(compositionDuration - 1, progress = 0.998f, iteration = 1, iterations = 2) |
| assertFrame(compositionDuration, progress = 0f, iteration = 2, iterations = 2) |
| assertFrame((2 * compositionDuration) - 1, progress = 0.998f, iteration = 2, iterations = 2) |
| assertFrame(2 * compositionDuration, progress = 1f, iteration = 2, iterations = 2, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testJumpsFromOneIterationToThree() = runTest { |
| val job = launch { |
| anim.animate(composition, iterations = 3) |
| } |
| assertEquals(0f, anim.progress) |
| clock.frameMs(0) |
| |
| assertFrame(2 * compositionDuration + 300, progress = 0.5f, iteration = 3, iterations = 3) |
| job.cancel() |
| } |
| |
| @Test |
| fun testCancels() = runTest { |
| val job = launch { |
| anim.animate(composition) |
| } |
| assertEquals(0f, anim.progress) |
| clock.frameMs(0) |
| |
| assertFrame(300, progress = 0.5f) |
| job.cancel() |
| assertFrame(compositionDuration - 1, progress = 0.5f, isPlaying = false, lastFrameNanos = 300000000L) |
| } |
| |
| @Test |
| fun testReverse() = runTest { |
| launch { |
| anim.animate(composition, speed = -1f) |
| } |
| assertFrame(0, progress = 1f, speed = -1f) |
| assertFrame(200, progress = 2 / 3f, speed = -1f) |
| assertFrame(compositionDuration - 1, progress = 0.0016f, speed = -1f) |
| assertFrame(compositionDuration, progress = 0f, isAtEnd = true, isPlaying = false, speed = -1f) |
| } |
| |
| @Test |
| fun testClipSpec() = runTest { |
| val clipSpec = LottieClipSpec.Progress(0.25f, 0.75f) |
| launch { |
| anim.animate(composition, clipSpec = clipSpec) |
| } |
| assertFrame(0, progress = 0.25f, clipSpec = clipSpec) |
| assertFrame(299, progress = 0.749f, clipSpec = clipSpec) |
| assertFrame(300, progress = 0.75f, isPlaying = false, isAtEnd = true, clipSpec = clipSpec) |
| } |
| |
| @Test |
| fun testClipSpecWithTwoIterations() = runTest { |
| val clipSpec = LottieClipSpec.Progress(0.25f, 0.75f) |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, iterations = 2) |
| } |
| assertFrame(0, progress = 0.25f, clipSpec = clipSpec, iterations = 2) |
| assertFrame(299, progress = 0.749f, clipSpec = clipSpec, iterations = 2) |
| assertFrame(598, progress = 0.748f, iteration = 2, clipSpec = clipSpec, iterations = 2) |
| assertFrame(599, progress = 0.75f, iteration = 2, isPlaying = false, isAtEnd = true, clipSpec = clipSpec, iterations = 2) |
| } |
| |
| @Test |
| fun testNegativeSpeedWithClipSpec() = runTest { |
| val clipSpec = LottieClipSpec.Progress(0.25f, 0.75f) |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, speed = -1f) |
| } |
| assertFrame(0, progress = 0.75f, clipSpec = clipSpec, speed = -1f) |
| assertFrame(299, progress = 0.2508f, clipSpec = clipSpec, speed = -1f) |
| assertFrame(300, progress = 0.25f, isPlaying = false, isAtEnd = true, clipSpec = clipSpec, speed = -1f) |
| } |
| |
| @Test |
| fun testChangingEndClipSpec() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertFrame(0, progress = 0f) |
| assertFrame(300, progress = 0.5f) |
| val clipSpec = LottieClipSpec.Progress(max = 0.75f) |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, continueFromPreviousAnimate = false) |
| } |
| assertFrame(316, progress = 0f, clipSpec = clipSpec) |
| assertFrame(616, progress = 0.5f, clipSpec = clipSpec) |
| assertFrame(800, progress = 0.75f, clipSpec = clipSpec, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testChangingBeginningClipSpec() = runTest { |
| launch { |
| anim.animate(composition, iterations = 2) |
| } |
| assertFrame(0, progress = 0f, iterations = 2) |
| assertFrame(300, progress = 0.5f, iterations = 2) |
| val clipSpec = LottieClipSpec.Progress(min = 0.25f) |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, initialProgress = anim.progress, continueFromPreviousAnimate = true) |
| } |
| assertFrame(598, progress = 0.998f, clipSpec = clipSpec, iterations = 2) |
| assertFrame(599, progress = 0.25f, clipSpec = clipSpec, iteration = 2, iterations = 2) |
| assertFrame(1048, progress = 0.999f, clipSpec = clipSpec, iteration = 2, iterations = 2) |
| assertFrame(1049, progress = 1f, clipSpec = clipSpec, iteration = 2, iterations = 2, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testResumingAnimation() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertFrame(0, progress = 0f) |
| assertFrame(300, progress = 0.5f) |
| val clipSpec = LottieClipSpec.Progress(max = 0.75f) |
| launch { |
| anim.animate( |
| composition, |
| clipSpec = clipSpec, |
| initialProgress = anim.progress, |
| continueFromPreviousAnimate = true, |
| ) |
| } |
| assertFrame(316, progress = 0.528f, clipSpec = clipSpec) |
| assertFrame(449, progress = 0.749f, clipSpec = clipSpec) |
| assertFrame(450, progress = 0.75f, clipSpec = clipSpec, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testReRunAnimation() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertFrame(0, progress = 0f) |
| assertFrame(300, progress = 0.5f) |
| launch { |
| anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true) |
| } |
| assertFrame(300, progress = 0.5f) |
| assertFrame(598, progress = 0.998f) |
| assertFrame(599, progress = 1f, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testSnapNoopToThenResume() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertFrame(0, progress = 0f) |
| assertFrame(300, progress = 0.5f) |
| launch { |
| anim.snapTo(composition) |
| anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true) |
| } |
| assertFrame(300, progress = 0.5f) |
| assertFrame(598, progress = 0.998f) |
| assertFrame(599, progress = 1f, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testSnapToThenResume() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertFrame(0, progress = 0f) |
| assertFrame(300, progress = 0.5f) |
| launch { |
| anim.snapTo(composition, progress = 0.2f) |
| anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true) |
| } |
| assertFrame(316, progress = 0.2f) |
| assertFrame(449, progress = 0.422f) |
| assertFrame(795, progress = 0.999f) |
| assertFrame(796, progress = 1f, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testSnapToAnotherIterationThenResume() = runTest { |
| launch { |
| anim.animate(composition, iterations = 3) |
| } |
| assertFrame(0, progress = 0f, iterations = 3) |
| assertFrame(1796, progress = 0.998f, iteration = 3, iterations = 3) |
| launch { |
| anim.snapTo(iteration = 1) |
| anim.animate(composition, initialProgress = anim.progress, continueFromPreviousAnimate = true) |
| } |
| assertFrame(1796, progress = 0.998f, iteration = 1, iterations = 3) |
| assertFrame(1797, progress = 0f, iteration = 2, iterations = 3) |
| assertFrame(2994, progress = 0.998f, iteration = 3, iterations = 3) |
| assertFrame(2995, progress = 1f, isPlaying = false, isAtEnd = true, iteration = 3, iterations = 3) |
| } |
| |
| @Test |
| fun testChangeSpeed() = runTest { |
| launch { |
| anim.animate(composition) |
| } |
| assertFrame(0, progress = 0f) |
| assertFrame(300, progress = 0.5f) |
| launch { |
| anim.animate(composition, speed = 2f, initialProgress = anim.progress, continueFromPreviousAnimate = true) |
| } |
| assertFrame(316, progress = 0.554f, speed = 2f) |
| assertFrame(449, progress = 0.998f, speed = 2f) |
| assertFrame(450, progress = 1f, speed = 2f, isPlaying = false, isAtEnd = true) |
| } |
| |
| @Test |
| fun testInfiniteSpeed() { |
| val clipSpec = LottieClipSpec.Progress(0.33f, 0.57f) |
| runTest { |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, speed = Float.POSITIVE_INFINITY, iterations = LottieConstants.IterateForever) |
| } |
| assertFrame( |
| 0, |
| progress = 0.57f, |
| isPlaying = false, |
| speed = Float.POSITIVE_INFINITY, |
| clipSpec = clipSpec, |
| isAtEnd = true, |
| iterations = LottieConstants.IterateForever, |
| iteration = LottieConstants.IterateForever, |
| lastFrameNanos = AnimationConstants.UnspecifiedTime, |
| ) |
| } |
| } |
| |
| @Test |
| fun testInfiniteSpeedWithIterations() { |
| val clipSpec = LottieClipSpec.Progress(0.33f, 0.57f) |
| runTest { |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, speed = Float.POSITIVE_INFINITY, iterations = 3) |
| } |
| assertFrame( |
| 300, |
| progress = 0.57f, |
| isPlaying = false, |
| speed = Float.POSITIVE_INFINITY, |
| clipSpec = clipSpec, |
| isAtEnd = true, |
| iterations = 3, |
| iteration = 3, |
| lastFrameNanos = AnimationConstants.UnspecifiedTime, |
| ) |
| } |
| } |
| |
| @Test |
| fun testNegativeInfiniteSpeed() { |
| val clipSpec = LottieClipSpec.Progress(0.33f, 0.57f) |
| runTest { |
| launch { |
| anim.animate(composition, clipSpec = clipSpec, speed = Float.NEGATIVE_INFINITY, iterations = LottieConstants.IterateForever) |
| } |
| assertFrame( |
| 0, |
| progress = 0.33f, |
| isPlaying = false, |
| speed = Float.NEGATIVE_INFINITY, |
| clipSpec = clipSpec, |
| isAtEnd = true, |
| iterations = LottieConstants.IterateForever, |
| iteration = LottieConstants.IterateForever, |
| lastFrameNanos = AnimationConstants.UnspecifiedTime, |
| ) |
| } |
| } |
| |
| |
| @Test |
| fun testNonCancellable() = runTest { |
| val job = launch { |
| anim.animate(composition, cancellationBehavior = LottieCancellationBehavior.OnIterationFinish) |
| } |
| assertFrame(0, progress = 0f) |
| job.cancel() |
| assertFrame(300, progress = 0.5f) |
| assertFrame(599, progress = 1f, isAtEnd = true, isPlaying = false) |
| } |
| |
| @Test |
| fun testCancelWithMultipleIterations() = runTest { |
| val job = launch { |
| anim.animate(composition, cancellationBehavior = LottieCancellationBehavior.OnIterationFinish, iterations = 3) |
| } |
| assertFrame(0, progress = 0f, iterations = 3) |
| job.cancel() |
| assertFrame(300, progress = 0.5f, iterations = 3) |
| assertFrame(599, progress = 1f, isAtEnd = false, isPlaying = false, iterations = 3) |
| } |
| |
| @Test |
| fun testCompositionCreated() = runTest { |
| val clipSpec = LottieClipSpec.Frame(20, 25) |
| val job1 = launch { |
| anim.animate(null, clipSpec = clipSpec) |
| } |
| assertFrame(0, progress = 0f, clipSpec = clipSpec, isAtEnd = true, isPlaying = false, lastFrameNanos = AnimationConstants.UnspecifiedTime) |
| |
| job1.cancelAndJoin() |
| val job2 = launch { |
| anim.animate(composition, clipSpec = clipSpec, initialProgress = anim.progress) |
| } |
| |
| assertFrame(0, progress = 0.556f, clipSpec = clipSpec) |
| job2.cancelAndJoin() |
| } |
| |
| private suspend fun assertFrame( |
| frameTimeMs: Long, |
| progress: Float, |
| iteration: Int = 1, |
| iterations: Int = 1, |
| speed: Float = 1f, |
| clipSpec: LottieClipSpec? = null, |
| isAtEnd: Boolean = false, |
| isPlaying: Boolean = true, |
| lastFrameNanos: Long = frameTimeMs * 1_000_000, |
| ) { |
| clock.frameMs(frameTimeMs) |
| assertEquals("progress at %d".format(frameTimeMs), progress, anim.progress, 0.001f) |
| assertEquals("iteration at %d".format(frameTimeMs), iteration, anim.iteration) |
| assertEquals("iterations at %d".format(frameTimeMs), iterations, anim.iterations) |
| assertEquals("speed at %d".format(frameTimeMs), speed, anim.speed) |
| assertEquals("clipSpec at %d".format(frameTimeMs), clipSpec, anim.clipSpec) |
| assertEquals("isAtEnd at %d".format(frameTimeMs), isAtEnd, anim.isAtEnd) |
| assertEquals("isPlaying at %d".format(frameTimeMs), isPlaying, anim.isPlaying) |
| assertEquals("lastFrameNanos at %d".format(frameTimeMs), lastFrameNanos, anim.lastFrameNanos) |
| } |
| |
| private fun runTest(test: suspend CoroutineScope.() -> Unit) { |
| runBlockingTest { |
| withContext(clock) { |
| test() |
| } |
| } |
| } |
| } |