blob: ed661bc73ba374a5c149c11662d0522d198e33d8 [file] [log] [blame]
package com.airbnb.lottie.compose
import androidx.compose.runtime.MonotonicFrameClock
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.test.DelayController
import kotlin.coroutines.ContinuationInterceptor
private const val DefaultFrameDelay = 16_000_000L
/**
* Copied from
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt?q=TestMonotonicFrameClock
*/
@ExperimentalCoroutinesApi
fun TestMonotonicFrameClock(
coroutineScope: CoroutineScope,
frameDelayNanos: Long = DefaultFrameDelay
): TestMonotonicFrameClock = TestMonotonicFrameClock(
coroutineScope = coroutineScope,
delayController = coroutineScope.coroutineContext[ContinuationInterceptor].let { interceptor ->
requireNotNull(interceptor as? DelayController) {
"ContinuationInterceptor $interceptor of supplied scope must implement DelayController"
}
},
frameDelayNanos = frameDelayNanos
)
/**
* A [MonotonicFrameClock] with a time source controlled by a `kotlinx-coroutines-test`
* [DelayController]. This frame clock may be used to consistently drive time under controlled
* tests.
*
* Calls to [withFrameNanos] will schedule an upcoming frame [frameDelayNanos] nanoseconds in the
* future by launching into [coroutineScope] if such a frame has not yet been scheduled. The
* current frame time for [withFrameNanos] is provided by [delayController]. It is strongly
* suggested that [coroutineScope] contain the test dispatcher controlled by [delayController].
*/
@ExperimentalCoroutinesApi
class TestMonotonicFrameClock(
private val coroutineScope: CoroutineScope,
private val delayController: DelayController,
@get:Suppress("MethodNameUnits") // Nanos for high-precision animation clocks
val frameDelayNanos: Long = DefaultFrameDelay
) : MonotonicFrameClock {
private val lock = Any()
private val awaiters = mutableListOf<Awaiter<*>>()
private var posted = false
private class Awaiter<R>(
private val onFrame: (Long) -> R,
private val continuation: CancellableContinuation<R>
) {
fun runFrame(frameTimeNanos: Long): () -> Unit {
val result = runCatching { onFrame(frameTimeNanos) }
return { continuation.resumeWith(result) }
}
}
override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R =
suspendCancellableCoroutine { co ->
synchronized(lock) {
awaiters.add(Awaiter(onFrame, co))
maybeLaunchTickRunner()
}
}
private fun maybeLaunchTickRunner() {
if (!posted) {
posted = true
coroutineScope.launch {
delay(frameDelayMillis)
synchronized(lock) {
posted = false
val toRun = awaiters.toList()
awaiters.clear()
val frameTime = delayController.currentTime * 1_000_000
// In case of awaiters on an immediate dispatcher, run all frame callbacks
// before resuming any associated continuations with the results.
toRun.map { it.runFrame(frameTime) }.forEach { it() }
}
}
}
}
}
/**
* The frame delay time for the [TestMonotonicFrameClock] in milliseconds.
*/
@ExperimentalCoroutinesApi
val TestMonotonicFrameClock.frameDelayMillis: Long
get() = frameDelayNanos / 1_000_000