blob: 6846b12076489971a6ef84de7fd9e9af8ee78215 [file] [log] [blame]
/*
* 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.MonotonicFrameClock
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Uptime
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(JUnit4::class)
class SuspendAnimationTest {
@Test
fun animateFloatVariantTest() =
runBlocking {
val anim = TargetBasedAnimation(
spring(dampingRatio = Spring.DampingRatioMediumBouncy), Float.VectorConverter,
initialValue = 0f, targetValue = 1f
)
val clock = TestFrameClock()
val interval = 50
withContext(clock) {
// Put in a bunch of frames 50 milliseconds apart
for (frameTimeMillis in 0..5000 step interval) {
clock.frame(frameTimeMillis * 1_000_000L)
}
var playTimeMillis = 0L
animate(
0f, 1f, 0f,
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) { value, velocity ->
assertEquals(anim.getValue(playTimeMillis), value, 0.001f)
assertEquals(anim.getVelocity(playTimeMillis), velocity, 0.001f)
playTimeMillis += interval
}
}
}
@Test
fun animateGenericsVariantTest() =
runBlocking {
val from = Offset(666f, 321f)
val to = Offset(919f, 864f)
val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
TwoWayConverter(
convertToVector = { AnimationVector2D(it.x, it.y) },
convertFromVector = { Offset(it.v1, it.v2) }
)
val anim = TargetBasedAnimation(
tween(500), offsetToVector, initialValue = from, targetValue = to
)
val clock = TestFrameClock()
val interval = 50
withContext(clock) {
// Put in a bunch of frames 50 milliseconds apart
for (frameTimeMillis in 0..500 step interval) {
clock.frame(frameTimeMillis * 1_000_000L)
}
var playTimeMillis = 0L
animate(
offsetToVector,
from, to,
animationSpec = tween(500)
) { value, _ ->
val expectedValue = anim.getValue(playTimeMillis)
assertEquals(expectedValue.x, value.x, 0.001f)
assertEquals(expectedValue.y, value.y, 0.001f)
playTimeMillis += interval
}
}
}
@Test
fun animateDecayTest() =
runBlocking {
val from = 666f
val velocity = 999f
val anim = DecayAnimation(
FloatExponentialDecaySpec(),
initialValue = from, initialVelocity = velocity
)
val clock = TestFrameClock()
val interval = 50
withContext(clock) {
// Put in a bunch of frames 50 milliseconds apart
for (frameTimeMillis in 0..5000 step interval) {
clock.frame(frameTimeMillis * 1_000_000L)
}
var playTimeMillis = 0L
animateDecay(
from, velocity,
animationSpec = FloatExponentialDecaySpec()
) { value, velocity ->
assertEquals(anim.getValue(playTimeMillis), value, 0.001f)
assertEquals(anim.getVelocity(playTimeMillis), velocity, 0.001f)
playTimeMillis += interval
}
}
}
@Test
fun animateToTest() {
runBlocking {
val from = Offset(666f, 321f)
val to = Offset(919f, 864f)
val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
TwoWayConverter(
convertToVector = { AnimationVector2D(it.x, it.y) },
convertFromVector = { Offset(it.v1, it.v2) }
)
val anim = TargetBasedAnimation(
tween(500), offsetToVector, initialValue = from, targetValue = to
)
val clock = TestFrameClock()
val interval = 50
val animationState = AnimationState(
initialValue = from,
typeConverter = offsetToVector,
lastFrameTime = Uptime(0)
)
withContext(clock) {
// Put in a bunch of frames 50 milliseconds apart
for (frameTimeMillis in 100..1000 step interval) {
clock.frame(frameTimeMillis * 1_000_000L)
}
// The first frame should start at 100ms
var playTimeMillis = 0L
animationState.animateTo(
to,
animationSpec = tween(500),
sequentialAnimation = true
) {
assertTrue(animationState.isRunning)
assertTrue(isRunning)
val expectedValue = anim.getValue(playTimeMillis)
assertEquals(expectedValue.x, value.x, 0.001f)
assertEquals(expectedValue.y, value.y, 0.001f)
if (playTimeMillis == 0L) {
// First invocation to block when starting from last frame is always
// playtime = 0
playTimeMillis = 100L
} else {
playTimeMillis += interval
}
if (playTimeMillis == 300L) {
// Prematurely cancel the animation and check corresponding states
cancelAnimation()
assertFalse(animationState.isRunning)
assertFalse(isRunning)
}
}
// Check that no more frames happened after cancel()
assertEquals(playTimeMillis, 300L)
assertFalse(animationState.isRunning)
}
}
}
@Test
fun animateDecayOnAnimationStateTest() =
runBlocking {
val from = 9f
val initialVelocity = 20f
val anim = DecayAnimation(
FloatExponentialDecaySpec(),
initialValue = from, initialVelocity = initialVelocity
)
val clock = TestFrameClock()
val interval = 50
withContext(clock) {
// Put in a bunch of frames 50 milliseconds apart
for (frameTimeMillis in 0..5000 step interval) {
clock.frame(frameTimeMillis * 1_000_000L)
}
var playTimeMillis = 0L
val state = AnimationState(9f, 20f)
state.animateDecay(
FloatExponentialDecaySpec().generateDecayAnimationSpec()
) {
assertEquals(anim.getValue(playTimeMillis), value, 0.001f)
assertEquals(anim.getVelocity(playTimeMillis), velocity, 0.001f)
playTimeMillis += interval
assertEquals(value, state.value, 0.0001f)
assertEquals(velocity, state.velocity, 0.0001f)
}
}
}
internal class TestFrameClock : MonotonicFrameClock {
// Make the send non-blocking
private val frameCh = Channel<Long>(Channel.UNLIMITED)
suspend fun frame(frameTimeNanos: Long) {
frameCh.send(frameTimeNanos)
}
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
onFrame(frameCh.receive())
}
}