AnimationClocks.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.ui.test

import androidx.compose.animation.core.AnimationClockObservable
import androidx.compose.animation.core.MonotonicFrameAnimationClock
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext

/**
 * Interface for animation clocks that can report their idleness and can switch between ticking
 * automatically (e.g., if it's driven by the main loop of the host) and ticking manually.
 *
 * An idle clock is one that is currently not driving any animations. Typically, that means a
 * clock where no observers are registered. The idleness can be retrieved by [isIdle].
 *
 * Use [pauseClock] to switch from automatic ticking to manual ticking, [resumeClock] to switch
 * from manual to automatic with; and manually tick the clock with [advanceClock].
 */
interface TestAnimationClock : AnimationClockObservable {
    /**
     * Whether the clock is idle or not. An idle clock is one that is not driving animations,
     * which happens (1) when no observers are observing this clock, or (2) when the clock is
     * paused.
     */
    val isIdle: Boolean

    /**
     * Pauses the automatic ticking of the clock. The clock shall only tick in response to
     * [advanceClock], and shall continue ticking automatically when [resumeClock] is called.
     * It's safe to call this method when the clock is already paused.
     */
    fun pauseClock()

    /**
     * Resumes the automatic ticking of the clock. It's safe to call this method when the clock
     * is already resumed.
     */
    fun resumeClock()

    /**
     * Whether the clock is [paused][pauseClock] or [not][resumeClock].
     */
    val isPaused: Boolean

    /**
     * Advances the clock by the given number of [milliseconds]. It is safe to call this method
     * both when the clock is paused and resumed.
     */
    fun advanceClock(milliseconds: Long)
}

/**
 * Creates a [MonotonicFrameAnimationClock] from the given [clock]. A new coroutine scope is
 * created from the [coroutineContext], dispatching on the main thread and using the [clock] as
 * the frame clock.
 *
 * @see MonotonicFrameAnimationClock
 */
@ExperimentalTesting
fun monotonicFrameAnimationClockOf(
    coroutineContext: CoroutineContext,
    clock: MonotonicFrameClock
): MonotonicFrameAnimationClock =
    MonotonicFrameAnimationClock(
        CoroutineScope(coroutineContext + TestUiDispatcher.Main + clock)
    )