Prevent setting seekable transition playTime when no transition is running
`seekAnimations(playTimeNanos = 0)` is called after animations finish. It's triggered by the animation duration being updated to 0.
It seems reasonable for a transition that's not playing (`startTimeNanos == UnspecifiedTime`) to _stay_ not playing when seeked with a 0-duration animation, and `playTimeNanos = 0` ambiguously means 0% or 100% when the duration is 0. Setting that to 0 when there's no animation erroneously causes `isRunning` to always return true. This defends against that by checking for 0 duration and preventing calling seekToFraction when that's the case.
Fixes: 334275648
Test: Added test asserting parent seekable and child transition return correct isRunning before, during, and after animateTo
Change-Id: I227113399a9b6baaa3496eaa0a3661a7a11bebb3
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
index 32d573f..8efa4d2 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
@@ -71,6 +71,7 @@
import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -2474,6 +2475,46 @@
}
@Test
+ fun isRunningFalseAfterChildAnimatedVisibilityTransition() {
+ val seekableTransitionState = SeekableTransitionState(AnimStates.From)
+ lateinit var coroutineScope: CoroutineScope
+ lateinit var transition: Transition<AnimStates>
+ var animatedVisibilityTransition: Transition<*>? = null
+
+ rule.mainClock.autoAdvance = false
+
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ transition = rememberTransition(seekableTransitionState, label = "Test")
+ transition.AnimatedVisibility(
+ visible = { it == AnimStates.To },
+ ) {
+ animatedVisibilityTransition = this.transition
+ Box(Modifier.size(100.dp))
+ }
+ }
+ rule.runOnIdle {
+ assertFalse(transition.isRunning)
+ assertNull(animatedVisibilityTransition)
+ }
+
+ rule.runOnUiThread {
+ coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.To) }
+ }
+ rule.mainClock.advanceTimeBy(50)
+ rule.runOnIdle {
+ assertTrue(transition.isRunning)
+ assertTrue(animatedVisibilityTransition!!.isRunning)
+ }
+
+ rule.mainClock.advanceTimeBy(5000)
+ rule.runOnIdle {
+ assertFalse(transition.isRunning)
+ assertFalse(animatedVisibilityTransition!!.isRunning)
+ }
+ }
+
+ @Test
fun testCleanupAfterDispose() {
fun isObserving(): Boolean {
var active = false
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index b61ed67..1fc3a21 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -707,7 +707,7 @@
animation.animationSpecDuration =
((1.0 - animation.start[0]) * totalDurationNanos).roundToLong()
}
- } else {
+ } else if (totalDurationNanos != 0L) {
// seekTo() called with a fraction. If an animation is running, we can just wait
// for the animation to change the value. The fraction may not be the best way
// to advance a regular animation.