MonotonicFrameAnimationClock.kt
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.animation.core
import androidx.compose.runtime.dispatch.DefaultMonotonicFrameClock
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.runtime.dispatch.withFrameMillis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* A [MonotonicFrameAnimationClock] is an [AnimationClockObservable] that is built on top of the
* [MonotonicFrameClock] found in the given [scope]. Use this when you want to use APIs that
* require the old [AnimationClockObservable], but your clock is a coroutine based
* [MonotonicFrameClock]. If the scope doesn't contain a frame clock, the
* [DefaultMonotonicFrameClock] is used.
*
* Since a frame clock is a [coroutine element][kotlin.coroutines.CoroutineContext.Element], you
* can add it to an existing scope using composition:
* ```
* withContext(ManualFrameClock(0L) + CoroutineName("Scope with a manually driven clock")) {
* withFrameMillis {
* // ...
* }
* }
* ```
* Note that some coroutine contexts define a default frame clock, so make sure you add your
* frame clock to a context (`context + clock`) instead of the other way around (`clock + context`).
*/
class MonotonicFrameAnimationClock(
private val scope: CoroutineScope
) : AnimationClockObservable {
private val observers = mutableMapOf<AnimationClockObserver, Job>()
val hasObservers: Boolean
@Suppress("DEPRECATION_ERROR")
get() = synchronized(observers) {
observers.isNotEmpty()
}
override fun subscribe(observer: AnimationClockObserver) {
// Launch a new coroutine that keeps awaiting frames on the monotonic clock,
// until unsubscribe for that observer is called.
@Suppress("DEPRECATION_ERROR")
synchronized(observers) {
observers[observer] = scope.launch {
val clock = coroutineContext[MonotonicFrameClock] ?: DefaultMonotonicFrameClock
// ManualAnimationClock might send the current time when a subscriber subscribes.
// ManualFrameClock doesn't, because there's no concept of subscription. Fix this
// in the glue between MonotonicFrameClock and AnimationClockObservable.
if (clock is ManualFrameClock && clock.dispatchOnSubscribe) {
observer.onAnimationFrame(clock.currentTime / 1_000_000)
}
while (true) {
clock.withFrameMillis { millis ->
observer.onAnimationFrame(millis)
}
}
}
}
}
override fun unsubscribe(observer: AnimationClockObserver) {
@Suppress("DEPRECATION_ERROR")
synchronized(observers) {
observers.remove(observer)?.cancel()
}
}
}