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.