1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.compose.animation.core
17 
18 import androidx.compose.animation.AnimatedContent
19 import androidx.compose.animation.AnimatedVisibility
20 import androidx.compose.animation.ExperimentalAnimationApi
21 import androidx.compose.animation.fadeIn
22 import androidx.compose.animation.fadeOut
23 import androidx.compose.animation.togetherWith
24 import androidx.compose.foundation.background
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.Column
27 import androidx.compose.foundation.layout.fillMaxSize
28 import androidx.compose.foundation.layout.fillMaxWidth
29 import androidx.compose.foundation.layout.requiredSize
30 import androidx.compose.foundation.layout.size
31 import androidx.compose.material3.Text
32 import androidx.compose.runtime.CompositionLocalProvider
33 import androidx.compose.runtime.LaunchedEffect
34 import androidx.compose.runtime.Stable
35 import androidx.compose.runtime.State
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.key
38 import androidx.compose.runtime.mutableFloatStateOf
39 import androidx.compose.runtime.mutableIntStateOf
40 import androidx.compose.runtime.mutableLongStateOf
41 import androidx.compose.runtime.mutableStateListOf
42 import androidx.compose.runtime.mutableStateOf
43 import androidx.compose.runtime.remember
44 import androidx.compose.runtime.rememberCoroutineScope
45 import androidx.compose.runtime.setValue
46 import androidx.compose.runtime.snapshotFlow
47 import androidx.compose.testutils.assertPixels
48 import androidx.compose.ui.Alignment
49 import androidx.compose.ui.Modifier
50 import androidx.compose.ui.draw.drawBehind
51 import androidx.compose.ui.graphics.Color
52 import androidx.compose.ui.platform.LocalDensity
53 import androidx.compose.ui.platform.testTag
54 import androidx.compose.ui.test.assertIsDisplayed
55 import androidx.compose.ui.test.assertIsNotDisplayed
56 import androidx.compose.ui.test.captureToImage
57 import androidx.compose.ui.test.junit4.createComposeRule
58 import androidx.compose.ui.test.onNodeWithTag
59 import androidx.compose.ui.unit.Density
60 import androidx.compose.ui.unit.dp
61 import androidx.compose.ui.util.fastForEachReversed
62 import androidx.test.ext.junit.runners.AndroidJUnit4
63 import androidx.test.filters.MediumTest
64 import androidx.test.filters.SdkSuppress
65 import kotlin.math.roundToInt
66 import kotlinx.coroutines.CoroutineScope
67 import kotlinx.coroutines.android.awaitFrame
68 import kotlinx.coroutines.async
69 import kotlinx.coroutines.flow.collectLatest
70 import kotlinx.coroutines.launch
71 import kotlinx.coroutines.runBlocking
72 import leakcanary.DetectLeaksAfterTestSuccess
73 import org.junit.Assert.assertEquals
74 import org.junit.Assert.assertFalse
75 import org.junit.Assert.assertNull
76 import org.junit.Assert.assertTrue
77 import org.junit.Rule
78 import org.junit.Test
79 import org.junit.rules.RuleChain
80 import org.junit.runner.RunWith
81 
82 @RunWith(AndroidJUnit4::class)
83 @MediumTest
84 class SeekableTransitionStateTest {
85     private val rule = createComposeRule()
86 
87     // Detect leaks BEFORE and AFTER compose rule work
88     @get:Rule
89     val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess()).around(rule)
90 
91     private enum class AnimStates {
92         From,
93         To,
94         Other,
95     }
96 
97     @Test
98     fun seekFraction() {
99         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
100         var animatedValue by mutableIntStateOf(-1)
101 
102         rule.setContent {
103             LaunchedEffect(seekableTransitionState) {
104                 seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
105             }
106             val transition = rememberTransition(seekableTransitionState, label = "Test")
107             animatedValue =
108                 transition
109                     .animateInt(
110                         label = "Value",
111                         transitionSpec = { tween(easing = LinearEasing) }
112                     ) { state ->
113                         when (state) {
114                             AnimStates.From -> 0
115                             else -> 1000
116                         }
117                     }
118                     .value
119         }
120         rule.runOnIdle {
121             assertEquals(0, animatedValue)
122             runBlocking {
123                 seekableTransitionState.seekTo(fraction = 0.5f)
124                 assertEquals(0.5f, seekableTransitionState.fraction)
125             }
126         }
127         rule.runOnIdle {
128             assertEquals(500, animatedValue)
129             runBlocking {
130                 seekableTransitionState.seekTo(fraction = 1f)
131                 assertEquals(1f, seekableTransitionState.fraction)
132             }
133         }
134         rule.runOnIdle {
135             assertEquals(1000, animatedValue)
136             runBlocking {
137                 seekableTransitionState.seekTo(fraction = 0.5f)
138                 assertEquals(0.5f, seekableTransitionState.fraction)
139             }
140         }
141         rule.runOnIdle {
142             assertEquals(500, animatedValue)
143             runBlocking {
144                 seekableTransitionState.seekTo(fraction = 0f)
145                 assertEquals(0f, seekableTransitionState.fraction)
146             }
147         }
148         rule.runOnIdle { assertEquals(0, animatedValue) }
149     }
150 
151     @Test
152     fun animateToTarget() {
153         var animatedValue by mutableIntStateOf(-1)
154         var duration by mutableLongStateOf(0)
155         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
156         lateinit var coroutineScope: CoroutineScope
157 
158         rule.mainClock.autoAdvance = false
159 
160         rule.setContent {
161             LaunchedEffect(seekableTransitionState) {
162                 seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
163             }
164             coroutineScope = rememberCoroutineScope()
165             val transition = rememberTransition(seekableTransitionState, label = "Test")
166             animatedValue =
167                 transition
168                     .animateInt(
169                         label = "Value",
170                         transitionSpec = { tween(easing = LinearEasing) }
171                     ) { state ->
172                         when (state) {
173                             AnimStates.From -> 0
174                             else -> 1000
175                         }
176                     }
177                     .value
178             duration = transition.totalDurationNanos
179         }
180 
181         rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo()
182         val deferred1 =
183             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
184         rule.mainClock.advanceTimeByFrame() // one frame to set the start time
185         rule.mainClock.advanceTimeByFrame()
186 
187         var progressFraction = 0f
188         rule.runOnIdle {
189             assertTrue(seekableTransitionState.fraction > 0f)
190             progressFraction = seekableTransitionState.fraction
191         }
192 
193         rule.mainClock.advanceTimeByFrame()
194         rule.runOnIdle {
195             assertTrue(seekableTransitionState.fraction > progressFraction)
196             progressFraction = seekableTransitionState.fraction
197         }
198 
199         // interrupt the progress
200 
201         runBlocking { seekableTransitionState.seekTo(fraction = 0.5f) }
202 
203         rule.mainClock.advanceTimeByFrame()
204 
205         rule.runOnIdle {
206             assertTrue(deferred1.isCancelled)
207             // We've stopped animating after seeking
208             assertEquals(0.5f, seekableTransitionState.fraction)
209             assertEquals(500, animatedValue)
210         }
211 
212         // continue from the same place
213         val deferred2 =
214             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
215         rule.waitForIdle() // wait for coroutine to run
216         rule.mainClock.advanceTimeByFrame() // one frame to set the start time
217         rule.mainClock.advanceTimeByFrame()
218 
219         rule.runOnIdle {
220             // We've stopped animating after seeking
221             assertTrue(seekableTransitionState.fraction > 0.5f)
222             assertTrue(seekableTransitionState.fraction < 1f)
223         }
224 
225         rule.mainClock.advanceTimeBy(5000L)
226 
227         rule.runOnIdle {
228             assertTrue(deferred2.isCompleted)
229             assertEquals(0f, seekableTransitionState.fraction, 0f)
230             assertEquals(1000, animatedValue)
231         }
232     }
233 
234     @Test
235     fun updatedTransition() {
236         var animatedValue by mutableIntStateOf(-1)
237         var duration = -1L
238         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
239 
240         rule.setContent {
241             LaunchedEffect(seekableTransitionState) {
242                 seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
243             }
244             val transition = rememberTransition(seekableTransitionState, label = "Test")
245             animatedValue =
246                 transition
247                     .animateInt(
248                         label = "Value",
249                         transitionSpec = { tween(durationMillis = 200, easing = LinearEasing) }
250                     ) { state ->
251                         when (state) {
252                             AnimStates.From -> 0
253                             else -> 1000
254                         }
255                     }
256                     .value
257             transition.AnimatedContent(
258                 transitionSpec = {
259                     fadeIn(tween(durationMillis = 1000, easing = LinearEasing)) togetherWith
260                         fadeOut(tween(durationMillis = 1000, easing = LinearEasing))
261                 }
262             ) { state ->
263                 if (state == AnimStates.To) {
264                     Box(Modifier.size(100.dp))
265                 }
266             }
267             duration = transition.totalDurationNanos
268         }
269 
270         rule.runOnIdle {
271             assertEquals(1000_000_000L, duration)
272             assertEquals(0f, seekableTransitionState.fraction, 0f)
273         }
274 
275         runBlocking {
276             // Go to the middle
277             seekableTransitionState.seekTo(fraction = 0.5f)
278         }
279 
280         rule.runOnIdle {
281             assertEquals(1000, animatedValue)
282             assertEquals(0.5f, seekableTransitionState.fraction)
283         }
284 
285         runBlocking {
286             // Go to the end
287             seekableTransitionState.seekTo(fraction = 1f)
288         }
289 
290         rule.runOnIdle {
291             assertEquals(1000, animatedValue)
292             assertEquals(1f, seekableTransitionState.fraction)
293         }
294 
295         runBlocking {
296             // Go back to part way through the animatedValue
297             seekableTransitionState.seekTo(fraction = 0.1f)
298         }
299 
300         rule.runOnIdle {
301             assertEquals(500, animatedValue)
302             assertEquals(0.1f, seekableTransitionState.fraction)
303         }
304     }
305 
306     @Test
307     fun repeatAnimate() {
308         var animatedValue by mutableIntStateOf(-1)
309         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
310         lateinit var coroutineScope: CoroutineScope
311 
312         rule.mainClock.autoAdvance = false
313 
314         rule.setContent {
315             coroutineScope = rememberCoroutineScope()
316             val transition = rememberTransition(seekableTransitionState, label = "Test")
317             animatedValue =
318                 transition
319                     .animateInt(
320                         label = "Value",
321                         transitionSpec = { tween(easing = LinearEasing) }
322                     ) { state ->
323                         when (state) {
324                             AnimStates.From -> 0
325                             else -> 1000
326                         }
327                     }
328                     .value
329         }
330 
331         val deferred1 =
332             rule.runOnUiThread {
333                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.To) }
334             }
335         rule.mainClock.advanceTimeByFrame() // one frame to set the start time
336         rule.mainClock.advanceTimeByFrame()
337 
338         // Running the same animation again should cancel the existing one
339         val deferred2 =
340             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
341 
342         rule.waitForIdle() // wait for coroutine to run
343         rule.mainClock.advanceTimeByFrame()
344 
345         assertTrue(deferred1.isCancelled)
346         assertFalse(deferred2.isCancelled)
347 
348         // seeking should cancel the animation
349         val deferred3 =
350             rule.runOnUiThread {
351                 coroutineScope.async { seekableTransitionState.seekTo(fraction = 0.25f) }
352             }
353 
354         rule.waitForIdle() // wait for coroutine to run
355         rule.mainClock.advanceTimeByFrame()
356 
357         assertTrue(deferred2.isCancelled)
358         assertFalse(deferred3.isCancelled)
359         assertTrue(deferred3.isCompleted)
360 
361         // start the animation again
362         val deferred4 =
363             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
364 
365         rule.waitForIdle() // wait for coroutine to run
366         rule.mainClock.advanceTimeByFrame()
367 
368         assertFalse(deferred4.isCancelled)
369     }
370 
371     @Test
372     fun segmentInitialized() {
373         var animatedValue by mutableIntStateOf(-1)
374         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
375         lateinit var segment: Transition.Segment<AnimStates>
376 
377         rule.setContent {
378             LaunchedEffect(seekableTransitionState) {
379                 seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
380             }
381             val transition = rememberTransition(seekableTransitionState, label = "Test")
382             animatedValue =
383                 transition
384                     .animateInt(
385                         label = "Value",
386                         transitionSpec = {
387                             if (initialState == targetState) {
388                                 snap()
389                             } else {
390                                 tween(easing = LinearEasing)
391                             }
392                         }
393                     ) { state ->
394                         when (state) {
395                             AnimStates.From -> 0
396                             else -> 1000
397                         }
398                     }
399                     .value
400             segment = transition.segment
401         }
402 
403         rule.runOnIdle {
404             assertEquals(AnimStates.From, segment.initialState)
405             assertEquals(AnimStates.To, segment.targetState)
406         }
407     }
408 
409     // In the middle of seeking from From to To, seek to Other
410     @Test
411     fun seekThirdState() {
412         rule.mainClock.autoAdvance = false
413         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
414         var animatedValue1 by mutableIntStateOf(-1)
415         var animatedValue2 by mutableIntStateOf(-1)
416         var animatedValue3 by mutableIntStateOf(-1)
417         lateinit var coroutineScope: CoroutineScope
418 
419         rule.setContent {
420             coroutineScope = rememberCoroutineScope()
421             LaunchedEffect(seekableTransitionState) {
422                 seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
423             }
424             val transition = rememberTransition(seekableTransitionState, label = "Test")
425             val val1 =
426                 transition.animateInt(
427                     label = "Value",
428                     transitionSpec = { tween(easing = LinearEasing) }
429                 ) { state ->
430                     when (state) {
431                         AnimStates.From -> 0
432                         else -> 1000
433                     }
434                 }
435             val val2 =
436                 transition.animateInt(
437                     label = "Value",
438                     transitionSpec = { tween(easing = LinearEasing) }
439                 ) { state ->
440                     when (state) {
441                         AnimStates.Other -> 1000
442                         else -> 0
443                     }
444                 }
445             val val3 =
446                 transition.animateInt(
447                     label = "Value",
448                     transitionSpec = { tween(easing = LinearEasing) }
449                 ) { state ->
450                     when (state) {
451                         AnimStates.From -> 0
452                         AnimStates.To -> 1000
453                         AnimStates.Other -> 2000
454                     }
455                 }
456             Box(
457                 Modifier.fillMaxSize().drawBehind {
458                     animatedValue1 = val1.value
459                     animatedValue2 = val2.value
460                     animatedValue3 = val3.value
461                 }
462             )
463         }
464         rule.mainClock.advanceTimeByFrame() // let seekTo() run
465         rule.runOnIdle {
466             // Check initial values
467             assertEquals(0, animatedValue1)
468             assertEquals(0, animatedValue2)
469             assertEquals(0, animatedValue3)
470             // Seek half way
471             runBlocking {
472                 seekableTransitionState.seekTo(fraction = 0.5f)
473                 assertEquals(0.5f, seekableTransitionState.fraction)
474             }
475         }
476         rule.mainClock.advanceTimeByFrame()
477         rule.runOnIdle {
478             // Check half way values
479             assertEquals(500, animatedValue1)
480             assertEquals(0, animatedValue2)
481             assertEquals(500, animatedValue3)
482         }
483         // Start seek to new state. It won't complete until the initial state is
484         // animated to "To"
485         val seekTo =
486             rule.runOnUiThread {
487                 coroutineScope.async {
488                     seekableTransitionState.seekTo(0f, targetState = AnimStates.Other)
489                 }
490             }
491         rule.mainClock.advanceTimeByFrame() // must recompose to Other
492         rule.runOnIdle {
493             assertEquals(AnimStates.Other, seekableTransitionState.targetState)
494             // First frame, nothing has changed. We've only gathered the first frame of the
495             // animation since it was not previously animating
496             assertEquals(500, animatedValue1)
497             assertEquals(0, animatedValue2)
498             assertEquals(500, animatedValue3)
499         }
500 
501         // Continue the initial value animation. It should use a linear animation.
502         rule.mainClock.advanceTimeBy(80L) // 4 frames of animation
503         rule.runOnIdle {
504             assertEquals(500 + (500f * 80f / 150f), animatedValue1.toFloat(), 1f)
505             assertEquals(0, animatedValue2)
506             assertEquals(500 + (500f * 80f / 150f), animatedValue3.toFloat(), 1f)
507         }
508         val seekToFraction =
509             rule.runOnUiThread {
510                 coroutineScope.async {
511                     seekableTransitionState.seekTo(fraction = 0.5f)
512                     assertEquals(0.5f, seekableTransitionState.fraction)
513                 }
514             }
515         rule.mainClock.advanceTimeByFrame()
516         rule.runOnIdle {
517             val expected1Value = 500 + (500f * 96f / 150f)
518             assertEquals(expected1Value, animatedValue1.toFloat(), 1f)
519             assertEquals(500, animatedValue2)
520             assertEquals(
521                 expected1Value + 0.5f * (2000 - expected1Value),
522                 animatedValue3.toFloat(),
523                 1f
524             )
525         }
526 
527         // Advance to the end of the seekTo() animation
528         rule.mainClock.advanceTimeBy(5_000)
529         runBlocking { seekToFraction.await() }
530         assertTrue(seekTo.isCancelled)
531         rule.runOnIdle {
532             // The initial values should be 1000/0/1000
533             // Target values should be 1000, 1000, 2000
534             // The seek is 0.5
535             assertEquals(1000, animatedValue1)
536             assertEquals(500, animatedValue2)
537             assertEquals(1500, animatedValue3)
538             runBlocking { seekableTransitionState.seekTo(fraction = 1f) }
539         }
540         rule.mainClock.advanceTimeByFrame()
541         rule.runOnIdle {
542             // Should be at the target values now
543             assertEquals(1000, animatedValue1)
544             assertEquals(1000, animatedValue2)
545             assertEquals(2000, animatedValue3)
546         }
547     }
548 
549     // In the middle of animating from From to To, seek to Other
550     @Test
551     fun interruptAnimationWithSeekThirdState() {
552         rule.mainClock.autoAdvance = false
553         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
554         var animatedValue1 by mutableIntStateOf(-1)
555         var animatedValue2 by mutableIntStateOf(-1)
556         var animatedValue3 by mutableIntStateOf(-1)
557         lateinit var coroutineScope: CoroutineScope
558 
559         rule.setContent {
560             coroutineScope = rememberCoroutineScope()
561             LaunchedEffect(seekableTransitionState) {
562                 seekableTransitionState.animateTo(AnimStates.To)
563             }
564             val transition = rememberTransition(seekableTransitionState, label = "Test")
565             val val1 =
566                 transition.animateInt(
567                     label = "Value",
568                     transitionSpec = { tween(easing = LinearEasing) }
569                 ) { state ->
570                     when (state) {
571                         AnimStates.From -> 0
572                         else -> 1000
573                     }
574                 }
575             val val2 =
576                 transition.animateInt(
577                     label = "Value",
578                     transitionSpec = { tween(easing = LinearEasing) }
579                 ) { state ->
580                     when (state) {
581                         AnimStates.Other -> 1000
582                         else -> 0
583                     }
584                 }
585             val val3 =
586                 transition.animateInt(
587                     label = "Value",
588                     transitionSpec = { tween(easing = LinearEasing) }
589                 ) { state ->
590                     when (state) {
591                         AnimStates.From -> 0
592                         AnimStates.To -> 1000
593                         AnimStates.Other -> 2000
594                     }
595                 }
596             Box(
597                 Modifier.fillMaxSize().drawBehind {
598                     animatedValue1 = val1.value
599                     animatedValue2 = val2.value
600                     animatedValue3 = val3.value
601                 }
602             )
603         }
604         rule.mainClock.advanceTimeByFrame() // lock in the animation start time
605         rule.runOnIdle {
606             assertEquals(0f, seekableTransitionState.fraction, 0.01f)
607             // Check initial values
608             assertEquals(0, animatedValue1)
609             assertEquals(0, animatedValue2)
610             assertEquals(0, animatedValue3)
611         }
612         // Advance around half way through the animation
613         rule.mainClock.advanceTimeBy(160)
614         rule.runOnIdle {
615             // should be 160/300 = 0.5333f
616             assertEquals(0.53f, seekableTransitionState.fraction, 0.01f)
617 
618             // Check values at that fraction
619             assertEquals(533f, animatedValue1.toFloat(), 1f)
620             assertEquals(0, animatedValue2)
621             assertEquals(533f, animatedValue3.toFloat(), 1f)
622         }
623 
624         val seekTo =
625             rule.runOnUiThread {
626                 coroutineScope.async {
627                     // seek to Other. This won't finish until the animation finishes
628                     seekableTransitionState.seekTo(0f, targetState = AnimStates.Other)
629                 }
630             }
631 
632         rule.runOnIdle {
633             // Nothing will have changed yet. The initial value should continue to animate
634             // after this
635             assertEquals(533f, animatedValue1.toFloat(), 1f)
636             assertEquals(0, animatedValue2)
637             assertEquals(533f, animatedValue3.toFloat(), 1f)
638         }
639 
640         // Advance time by two more frames
641         rule.mainClock.advanceTimeBy(32)
642         rule.runOnIdle {
643             // should be 192/300 = 0.64 through animation
644             assertEquals(640f, animatedValue1.toFloat(), 1f)
645             assertEquals(0, animatedValue2)
646             assertEquals(640f, animatedValue3.toFloat(), 1f)
647         }
648         val seekToHalf =
649             rule.runOnUiThread {
650                 coroutineScope.async {
651                     seekableTransitionState.seekTo(fraction = 0.5f)
652                     assertEquals(0.5f, seekableTransitionState.fraction)
653                 }
654             }
655         rule.runOnIdle { assertEquals(500, animatedValue2) }
656 
657         // Advance to the end of the seekTo() animation
658         rule.mainClock.advanceTimeBy(5_000)
659         assertTrue(seekToHalf.isCompleted)
660         assertTrue(seekTo.isCancelled)
661         rule.runOnIdle {
662             // The initial values should be 1000/0/1000
663             // Target values should be 1000, 1000, 2000
664             // The seek is 0.5
665             assertEquals(1000, animatedValue1)
666             assertEquals(500, animatedValue2)
667             assertEquals(1500, animatedValue3)
668         }
669         rule.runOnUiThread {
670             coroutineScope.launch {
671                 seekableTransitionState.seekTo(fraction = 1f)
672                 assertEquals(1f, seekableTransitionState.fraction, 0f)
673             }
674         }
675         rule.mainClock.advanceTimeByFrame()
676         rule.runOnIdle {
677             // Should be at the target values now
678             assertEquals(1000, animatedValue1)
679             assertEquals(1000, animatedValue2)
680             assertEquals(2000, animatedValue3)
681         }
682     }
683 
684     // In the middle of animating from From to To, seek to Other
685     @Test
686     fun interruptAnimationWithAnimateToThirdState() {
687         rule.mainClock.autoAdvance = false
688         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
689         var animatedValue1 by mutableIntStateOf(-1)
690         var animatedValue2 by mutableIntStateOf(-1)
691         var animatedValue3 by mutableIntStateOf(-1)
692         lateinit var coroutineScope: CoroutineScope
693 
694         rule.setContent {
695             coroutineScope = rememberCoroutineScope()
696             LaunchedEffect(seekableTransitionState) {
697                 seekableTransitionState.animateTo(AnimStates.To)
698             }
699             val transition = rememberTransition(seekableTransitionState, label = "Test")
700             val val1 =
701                 transition.animateInt(
702                     label = "Value",
703                     transitionSpec = { tween(easing = LinearEasing) }
704                 ) { state ->
705                     when (state) {
706                         AnimStates.From -> 0
707                         else -> 1000
708                     }
709                 }
710             val val2 =
711                 transition.animateInt(
712                     label = "Value",
713                     transitionSpec = { tween(easing = LinearEasing) }
714                 ) { state ->
715                     when (state) {
716                         AnimStates.Other -> 1000
717                         else -> 0
718                     }
719                 }
720             val val3 =
721                 transition.animateInt(
722                     label = "Value",
723                     transitionSpec = { tween(easing = LinearEasing) }
724                 ) { state ->
725                     when (state) {
726                         AnimStates.From -> 0
727                         AnimStates.To -> 1000
728                         AnimStates.Other -> 2000
729                     }
730                 }
731             Box(
732                 Modifier.fillMaxSize().drawBehind {
733                     animatedValue1 = val1.value
734                     animatedValue2 = val2.value
735                     animatedValue3 = val3.value
736                 }
737             )
738         }
739         rule.mainClock.advanceTimeByFrame() // lock in the animation start time
740         rule.runOnIdle {
741             assertEquals(0f, seekableTransitionState.fraction, 0.01f)
742             // Check initial values
743             assertEquals(0, animatedValue1)
744             assertEquals(0, animatedValue2)
745             assertEquals(0, animatedValue3)
746         }
747         // Advance around half way through the animation
748         rule.mainClock.advanceTimeBy(160)
749         rule.runOnIdle {
750             // should be 160/300 = 0.5333f
751             assertEquals(0.53f, seekableTransitionState.fraction, 0.01f)
752 
753             // Check values at that fraction
754             assertEquals(533f, animatedValue1.toFloat(), 1f)
755             assertEquals(0, animatedValue2)
756             assertEquals(533f, animatedValue3.toFloat(), 1f)
757         }
758         val animateToOther =
759             rule.runOnUiThread {
760                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.Other) }
761             }
762 
763         rule.mainClock.advanceTimeBy(16) // composition after animateTo()
764 
765         rule.runOnIdle {
766             // initial should be 176/300 = 0.587 through animation
767             assertEquals(586.7f, animatedValue1.toFloat(), 1f)
768             assertEquals(0, animatedValue2)
769             assertEquals(586.7f, animatedValue3.toFloat(), 1f)
770         }
771 
772         // Lock in the animation for the animation to Other, but advance animation to To
773         rule.mainClock.advanceTimeBy(16)
774         rule.runOnIdle {
775             // initial should be 192/300 = 0.640 through animation
776             // target should be 16/300 = 0.053
777             assertEquals(640f, animatedValue1.toFloat(), 1f)
778             assertEquals(53.3f, animatedValue2.toFloat(), 1f)
779             assertEquals(640f + ((2000f - 640f) * 0.053f), animatedValue3.toFloat(), 1f)
780         }
781 
782         // Advance time by two more frames
783         rule.mainClock.advanceTimeBy(32)
784         rule.runOnIdle {
785             // initial should be 224/300 = 0.746.7 through animation
786             // other should be 48/300 = 0.160 through the animation
787             assertEquals(746.7f, animatedValue1.toFloat(), 1f)
788             assertEquals(160f, animatedValue2.toFloat(), 1f)
789             assertEquals(746.7f + ((2000f - 746.7f) * 0.160f), animatedValue3.toFloat(), 2f)
790         }
791 
792         // Advance to the end of the animation
793         rule.mainClock.advanceTimeBy(5_000)
794         assertTrue(animateToOther.isCompleted)
795         rule.runOnIdle {
796             assertEquals(1000, animatedValue1)
797             assertEquals(1000, animatedValue2)
798             assertEquals(2000, animatedValue3)
799         }
800     }
801 
802     // In the middle of animating from From to To, seek to Other
803     @Test
804     fun cancelAnimationWithAnimateToThirdStateW() {
805         rule.mainClock.autoAdvance = false
806         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
807         var animatedValue1 by mutableIntStateOf(-1)
808         var animatedValue2 by mutableIntStateOf(-1)
809         var animatedValue3 by mutableIntStateOf(-1)
810         var targetState by mutableStateOf(AnimStates.To)
811 
812         rule.setContent {
813             LaunchedEffect(seekableTransitionState, targetState) {
814                 seekableTransitionState.animateTo(targetState)
815             }
816             val transition = rememberTransition(seekableTransitionState, label = "Test")
817             val val1 =
818                 transition.animateInt(
819                     label = "Value",
820                     transitionSpec = { tween(easing = LinearEasing) }
821                 ) { state ->
822                     when (state) {
823                         AnimStates.From -> 0
824                         else -> 1000
825                     }
826                 }
827             val val2 =
828                 transition.animateInt(
829                     label = "Value",
830                     transitionSpec = { tween(easing = LinearEasing) }
831                 ) { state ->
832                     when (state) {
833                         AnimStates.Other -> 1000
834                         else -> 0
835                     }
836                 }
837             val val3 =
838                 transition.animateInt(
839                     label = "Value",
840                     transitionSpec = { tween(easing = LinearEasing) }
841                 ) { state ->
842                     when (state) {
843                         AnimStates.From -> 0
844                         AnimStates.To -> 1000
845                         AnimStates.Other -> 2000
846                     }
847                 }
848             Box(
849                 Modifier.fillMaxSize().drawBehind {
850                     animatedValue1 = val1.value
851                     animatedValue2 = val2.value
852                     animatedValue3 = val3.value
853                 }
854             )
855         }
856         rule.mainClock.advanceTimeByFrame() // lock in the animation start time
857         rule.runOnIdle {
858             assertEquals(0f, seekableTransitionState.fraction, 0.01f)
859             // Check initial values
860             assertEquals(0, animatedValue1)
861             assertEquals(0, animatedValue2)
862             assertEquals(0, animatedValue3)
863         }
864         // Advance around half way through the animation
865         rule.mainClock.advanceTimeBy(160)
866         rule.runOnIdle {
867             // should be 160/300 = 0.5333f
868             assertEquals(0.53f, seekableTransitionState.fraction, 0.01f)
869 
870             // Check values at that fraction
871             assertEquals(533f, animatedValue1.toFloat(), 1f)
872             assertEquals(0, animatedValue2)
873             assertEquals(533f, animatedValue3.toFloat(), 1f)
874             targetState = AnimStates.Other
875         }
876 
877         // Advance the clock so that the LaunchedEffect can run
878         rule.mainClock.advanceTimeBy(16)
879 
880         rule.runOnIdle {
881             assertEquals(AnimStates.Other, seekableTransitionState.targetState)
882 
883             // The time is advanced first, so the values are updated, then the
884             // LaunchedEffect cancels the animation
885             assertEquals(586, animatedValue1)
886             assertEquals(0, animatedValue2)
887             assertEquals(586, animatedValue3)
888         }
889 
890         // Compose the change
891         rule.mainClock.advanceTimeBy(16)
892 
893         rule.runOnIdle {
894             // The previous animation's start time can be used, so continue the animation
895             assertEquals(640, animatedValue1)
896             assertEquals(0, animatedValue2) // animation hasn't started yet
897             assertEquals(640, animatedValue3) // animation hasn't started yet
898         }
899 
900         // Advance one frame
901         rule.mainClock.advanceTimeBy(16)
902         rule.runOnIdle {
903             assertEquals(693, animatedValue1)
904             assertEquals(53, animatedValue2)
905             assertEquals(693 + ((2000 - 693) * 16 / 300), animatedValue3)
906         }
907 
908         // Advance to the end of the animation
909         rule.mainClock.advanceTimeBy(5_000)
910         rule.runOnIdle {
911             assertEquals(1000, animatedValue1)
912             assertEquals(1000, animatedValue2)
913             assertEquals(2000, animatedValue3)
914         }
915     }
916 
917     @Test
918     fun interruptInterruption() {
919         rule.mainClock.autoAdvance = false
920         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
921         var animatedValue1 by mutableIntStateOf(-1)
922         var animatedValue2 by mutableIntStateOf(-1)
923         var animatedValue3 by mutableIntStateOf(-1)
924         lateinit var coroutineScope: CoroutineScope
925 
926         rule.setContent {
927             coroutineScope = rememberCoroutineScope()
928             val transition = rememberTransition(seekableTransitionState, label = "Test")
929             val val1 =
930                 transition.animateInt(
931                     label = "Value",
932                     transitionSpec = { tween(easing = LinearEasing) }
933                 ) { state ->
934                     when (state) {
935                         AnimStates.From -> 0
936                         else -> 1000
937                     }
938                 }
939             val val2 =
940                 transition.animateInt(
941                     label = "Value",
942                     transitionSpec = { tween(easing = LinearEasing) }
943                 ) { state ->
944                     when (state) {
945                         AnimStates.Other -> 1000
946                         else -> 0
947                     }
948                 }
949             val val3 =
950                 transition.animateInt(
951                     label = "Value",
952                     transitionSpec = { tween(easing = LinearEasing) }
953                 ) { state ->
954                     when (state) {
955                         AnimStates.From -> 0
956                         AnimStates.To -> 1000
957                         AnimStates.Other -> 2000
958                     }
959                 }
960             Box(
961                 Modifier.fillMaxSize().drawBehind {
962                     animatedValue1 = val1.value
963                     animatedValue2 = val2.value
964                     animatedValue3 = val3.value
965                 }
966             )
967         }
968         rule.waitForIdle()
969         rule.runOnUiThread {
970             coroutineScope.launch {
971                 seekableTransitionState.seekTo(0f, targetState = AnimStates.To)
972             }
973         }
974         rule.waitForIdle()
975         rule.runOnUiThread {
976             coroutineScope.launch { seekableTransitionState.seekTo(fraction = 0.5f) }
977         }
978         rule.waitForIdle()
979         rule.runOnUiThread {
980             coroutineScope.launch {
981                 seekableTransitionState.seekTo(0f, targetState = AnimStates.Other)
982             }
983         }
984         rule.waitForIdle()
985         rule.mainClock.advanceTimeByFrame() // lock in the initial value animation start time
986         rule.runOnUiThread {
987             coroutineScope.launch { seekableTransitionState.seekTo(fraction = 0.5f) }
988         }
989         rule.waitForIdle()
990         rule.runOnUiThread {
991             coroutineScope.launch {
992                 seekableTransitionState.seekTo(0f, targetState = AnimStates.From)
993             }
994         }
995         rule.waitForIdle()
996 
997         // Now we have two initial value animations running. One is for animating
998         // from From -> To, one from To -> Other
999         // The From -> To animation should affect animatedValue1 and animatedValue2
1000         // The To -> Other animation should affect animatedValue3
1001 
1002         // Holding the value here, the animations should move the values to 1000, 1000, 2000
1003         rule.mainClock.advanceTimeBy(5_000L)
1004         rule.runOnIdle {
1005             assertEquals(1000, animatedValue1)
1006             assertEquals(1000, animatedValue2)
1007             assertEquals(2000, animatedValue3)
1008         }
1009     }
1010 
1011     @OptIn(ExperimentalAnimationApi::class, InternalAnimationApi::class)
1012     @Test
1013     fun delayedTransition() {
1014         rule.mainClock.autoAdvance = false
1015         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1016         lateinit var coroutineScope: CoroutineScope
1017         lateinit var transition: Transition<AnimStates>
1018 
1019         rule.setContent {
1020             coroutineScope = rememberCoroutineScope()
1021             transition = rememberTransition(seekableTransitionState, label = "Test")
1022             transition.AnimatedVisibility(
1023                 visible = { it != AnimStates.To },
1024                 enter = fadeIn(tween(300, 0, LinearEasing)),
1025                 exit = fadeOut(tween(300, 0, LinearEasing))
1026             ) {
1027                 Box(Modifier.fillMaxSize().drawBehind { drawRect(Color.Red) })
1028             }
1029         }
1030         rule.waitForIdle()
1031         val seekTo =
1032             rule.runOnUiThread {
1033                 coroutineScope.async { seekableTransitionState.seekTo(0.5f, AnimStates.To) }
1034             }
1035         rule.mainClock.advanceTimeByFrame()
1036         rule.runOnIdle {
1037             assertTrue(seekTo.isCompleted)
1038             assertEquals(150L * MillisToNanos, transition.playTimeNanos)
1039         }
1040     }
1041 
1042     @Test
1043     fun seekAfterAnimating() {
1044         rule.mainClock.autoAdvance = false
1045         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1046         var animatedValue1 by mutableIntStateOf(-1)
1047         lateinit var coroutineScope: CoroutineScope
1048 
1049         rule.setContent {
1050             coroutineScope = rememberCoroutineScope()
1051             val transition = rememberTransition(seekableTransitionState, label = "Test")
1052             val val1 =
1053                 transition.animateInt(
1054                     label = "Value",
1055                     transitionSpec = { tween(easing = LinearEasing) }
1056                 ) { state ->
1057                     when (state) {
1058                         AnimStates.From -> 0
1059                         else -> 1000
1060                     }
1061                 }
1062             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1063         }
1064         rule.waitForIdle()
1065         val deferred =
1066             rule.runOnUiThread {
1067                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.To) }
1068             }
1069         rule.mainClock.advanceTimeBy(10_000L) // complete the animation
1070         rule.waitForIdle()
1071         assertTrue(deferred.isCompleted)
1072         assertEquals(1000, animatedValue1)
1073 
1074         // seeking after the animation has completed should not change any value
1075         rule.runOnIdle { coroutineScope.launch { seekableTransitionState.seekTo(fraction = 0.5f) } }
1076         rule.waitForIdle()
1077         rule.mainClock.advanceTimeByFrame()
1078         assertEquals(1000, animatedValue1)
1079     }
1080 
1081     @Test
1082     fun animateToWithSpec() {
1083         rule.mainClock.autoAdvance = false
1084         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1085         var animatedValue1 by mutableIntStateOf(-1)
1086         lateinit var coroutineScope: CoroutineScope
1087 
1088         rule.setContent {
1089             coroutineScope = rememberCoroutineScope()
1090             val transition = rememberTransition(seekableTransitionState, label = "Test")
1091             val val1 =
1092                 transition.animateInt(
1093                     label = "Value",
1094                     transitionSpec = { tween(durationMillis = 100, easing = LinearEasing) }
1095                 ) { state ->
1096                     when (state) {
1097                         AnimStates.From -> 0
1098                         else -> 1000
1099                     }
1100                 }
1101             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1102         }
1103         rule.waitForIdle()
1104         rule.runOnUiThread {
1105             coroutineScope.launch { seekableTransitionState.seekTo(0.5f, AnimStates.To) }
1106         }
1107         rule.waitForIdle()
1108         rule.mainClock.advanceTimeByFrame()
1109         val deferred =
1110             rule.runOnUiThread {
1111                 coroutineScope.async {
1112                     seekableTransitionState.animateTo(animationSpec = tween(1000, 0, LinearEasing))
1113                 }
1114             }
1115         rule.mainClock.advanceTimeByFrame() // lock in the start time
1116         rule.mainClock.advanceTimeBy(64)
1117         rule.runOnIdle {
1118             // should be 500 + 500 * 64/1000 = 532
1119             assertEquals(532, animatedValue1)
1120         }
1121         rule.mainClock.advanceTimeBy(192)
1122         rule.runOnIdle {
1123             // should be 500 + 500 * 256/1000 = 628
1124             assertEquals(628, animatedValue1)
1125         }
1126         rule.mainClock.advanceTimeBy(256)
1127         rule.runOnIdle {
1128             // should be 500 + 500 * 512/1000 = 756
1129             assertEquals(756, animatedValue1)
1130         }
1131         rule.mainClock.advanceTimeBy(512)
1132         rule.runOnIdle {
1133             assertTrue(deferred.isCompleted)
1134             assertEquals(1000, animatedValue1)
1135         }
1136     }
1137 
1138     @Test
1139     fun seekToFollowedByAnimation() {
1140         rule.mainClock.autoAdvance = false
1141         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1142         var animatedValue1 by mutableIntStateOf(-1)
1143         lateinit var coroutineScope: CoroutineScope
1144 
1145         rule.setContent {
1146             coroutineScope = rememberCoroutineScope()
1147             val transition = rememberTransition(seekableTransitionState, label = "Test")
1148             val val1 =
1149                 transition.animateInt(
1150                     label = "Value",
1151                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1152                 ) { state ->
1153                     when (state) {
1154                         AnimStates.From -> 0
1155                         else -> 1000
1156                     }
1157                 }
1158             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1159         }
1160         rule.waitForIdle()
1161         rule.runOnUiThread {
1162             coroutineScope.launch {
1163                 seekableTransitionState.seekTo(1f, AnimStates.To)
1164                 seekableTransitionState.animateTo(AnimStates.From)
1165             }
1166         }
1167         rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
1168         rule.runOnIdle { // seekTo() should run now, setting the animated value
1169             assertEquals(1000, animatedValue1)
1170         }
1171         rule.mainClock.advanceTimeByFrame() // lock in the animation clock
1172         rule.runOnIdle { assertEquals(1000, animatedValue1) }
1173         rule.mainClock.advanceTimeByFrame()
1174         rule.runOnIdle { assertEquals(984, animatedValue1) }
1175         rule.mainClock.advanceTimeByFrame()
1176         rule.runOnIdle { assertEquals(968, animatedValue1) }
1177         rule.mainClock.advanceTimeBy(1000)
1178         rule.runOnIdle { assertEquals(0, animatedValue1) }
1179     }
1180 
1181     @Test
1182     fun conflictingSeekTo() {
1183         rule.mainClock.autoAdvance = false
1184         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1185         var animatedValue1 by mutableIntStateOf(-1)
1186         lateinit var coroutineScope: CoroutineScope
1187 
1188         rule.setContent {
1189             coroutineScope = rememberCoroutineScope()
1190             val transition = rememberTransition(seekableTransitionState, label = "Test")
1191             val val1 =
1192                 transition.animateInt(
1193                     label = "Value",
1194                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1195                 ) { state ->
1196                     val target =
1197                         when (state) {
1198                             AnimStates.From -> 0
1199                             AnimStates.Other -> 2000
1200                             else -> 1000
1201                         }
1202                     target
1203                 }
1204             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1205         }
1206         rule.waitForIdle()
1207         val defer1 =
1208             rule.runOnUiThread {
1209                 coroutineScope.async {
1210                     seekableTransitionState.seekTo(1f, AnimStates.To)
1211                     seekableTransitionState.animateTo(AnimStates.From)
1212                 }
1213             }
1214         val defer2 =
1215             rule.runOnUiThread {
1216                 coroutineScope.async {
1217                     seekableTransitionState.seekTo(1f, AnimStates.Other)
1218                     seekableTransitionState.animateTo(AnimStates.From)
1219                 }
1220             }
1221         rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
1222         rule.runOnIdle {
1223             assertTrue(defer1.isCancelled)
1224             assertFalse(defer2.isCancelled)
1225             assertEquals(2000, animatedValue1)
1226         }
1227         rule.mainClock.advanceTimeByFrame() // lock in the animation clock
1228         rule.runOnIdle { assertEquals(2000, animatedValue1) }
1229         rule.mainClock.advanceTimeByFrame()
1230         rule.runOnIdle { assertEquals(1968, animatedValue1) }
1231         rule.mainClock.advanceTimeBy(1000)
1232         rule.runOnIdle {
1233             assertEquals(0, animatedValue1)
1234             assertTrue(defer2.isCompleted)
1235         }
1236     }
1237 
1238     @Test
1239     fun conflictingSnapTo() {
1240         rule.mainClock.autoAdvance = false
1241         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1242         var animatedValue1 by mutableIntStateOf(-1)
1243         lateinit var coroutineScope: CoroutineScope
1244 
1245         rule.setContent {
1246             coroutineScope = rememberCoroutineScope()
1247             val transition = rememberTransition(seekableTransitionState, label = "Test")
1248             val val1 =
1249                 transition.animateInt(
1250                     label = "Value",
1251                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1252                 ) { state ->
1253                     val target =
1254                         when (state) {
1255                             AnimStates.From -> 0
1256                             AnimStates.Other -> 2000
1257                             else -> 1000
1258                         }
1259                     target
1260                 }
1261             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1262         }
1263         rule.waitForIdle()
1264         val defer1 =
1265             rule.runOnUiThread {
1266                 coroutineScope.async {
1267                     seekableTransitionState.snapTo(AnimStates.To)
1268                     seekableTransitionState.animateTo(AnimStates.From)
1269                 }
1270             }
1271         val defer2 =
1272             rule.runOnUiThread {
1273                 coroutineScope.async {
1274                     seekableTransitionState.snapTo(AnimStates.Other)
1275                     seekableTransitionState.animateTo(AnimStates.From)
1276                 }
1277             }
1278         rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
1279         rule.runOnIdle {
1280             assertTrue(defer1.isCancelled)
1281             assertFalse(defer2.isCancelled)
1282             assertEquals(2000, animatedValue1)
1283         }
1284         rule.mainClock.advanceTimeByFrame() // lock in the animation clock
1285         rule.runOnIdle { assertEquals(2000, animatedValue1) }
1286         rule.mainClock.advanceTimeByFrame()
1287         rule.runOnIdle { assertEquals(1968, animatedValue1) }
1288         rule.mainClock.advanceTimeBy(1000)
1289         rule.runOnIdle {
1290             assertEquals(0, animatedValue1)
1291             assertTrue(defer2.isCompleted)
1292         }
1293     }
1294 
1295     /**
1296      * Here, the first seekTo() doesn't do anything since the target is the same as the current
1297      * value. It only changes the fraction.
1298      */
1299     @Test
1300     fun conflictingSeekTo2() {
1301         rule.mainClock.autoAdvance = false
1302         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1303         var animatedValue1 by mutableIntStateOf(-1)
1304         lateinit var coroutineScope: CoroutineScope
1305 
1306         rule.setContent {
1307             coroutineScope = rememberCoroutineScope()
1308             val transition = rememberTransition(seekableTransitionState, label = "Test")
1309             val val1 =
1310                 transition.animateInt(
1311                     label = "Value",
1312                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1313                 ) { state ->
1314                     val target =
1315                         when (state) {
1316                             AnimStates.From -> 0
1317                             AnimStates.Other -> 2000
1318                             else -> 1000
1319                         }
1320                     target
1321                 }
1322             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1323         }
1324         rule.waitForIdle()
1325         rule.runOnUiThread {
1326             coroutineScope.launch {
1327                 seekableTransitionState.seekTo(1f, AnimStates.From)
1328                 seekableTransitionState.animateTo(AnimStates.To)
1329             }
1330             coroutineScope.launch {
1331                 seekableTransitionState.seekTo(1f, AnimStates.Other)
1332                 seekableTransitionState.animateTo(AnimStates.From)
1333             }
1334         }
1335         rule.mainClock.advanceTimeByFrame() // let the composition happen after seekTo
1336         rule.runOnIdle { assertEquals(2000, animatedValue1) }
1337         rule.mainClock.advanceTimeByFrame() // lock in the animation clock
1338         rule.runOnIdle { assertEquals(2000, animatedValue1) }
1339         rule.mainClock.advanceTimeByFrame()
1340         rule.runOnIdle { assertEquals(1968, animatedValue1) }
1341         rule.mainClock.advanceTimeBy(1000)
1342         rule.runOnIdle { assertEquals(0, animatedValue1) }
1343     }
1344 
1345     /**
1346      * Here, the first seekTo() doesn't do anything since the target is the same as the current
1347      * value. It only changes the fraction.
1348      */
1349     @Test
1350     fun conflictingSnapTo2() {
1351         rule.mainClock.autoAdvance = false
1352         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1353         var animatedValue1 by mutableIntStateOf(-1)
1354         lateinit var coroutineScope: CoroutineScope
1355 
1356         rule.setContent {
1357             coroutineScope = rememberCoroutineScope()
1358             val transition = rememberTransition(seekableTransitionState, label = "Test")
1359             val val1 =
1360                 transition.animateInt(
1361                     label = "Value",
1362                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1363                 ) { state ->
1364                     val target =
1365                         when (state) {
1366                             AnimStates.From -> 0
1367                             AnimStates.Other -> 2000
1368                             else -> 1000
1369                         }
1370                     target
1371                 }
1372             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1373         }
1374         rule.waitForIdle()
1375         rule.runOnUiThread {
1376             coroutineScope.launch {
1377                 seekableTransitionState.snapTo(AnimStates.From)
1378                 seekableTransitionState.animateTo(AnimStates.To)
1379             }
1380             coroutineScope.launch {
1381                 seekableTransitionState.snapTo(AnimStates.Other)
1382                 seekableTransitionState.animateTo(AnimStates.From)
1383             }
1384         }
1385         rule.mainClock.advanceTimeByFrame() // let the composition happen after snapTo
1386         rule.runOnIdle { assertEquals(2000, animatedValue1) }
1387         rule.mainClock.advanceTimeByFrame() // lock in the animation clock
1388         rule.runOnIdle { assertEquals(2000, animatedValue1) }
1389         rule.mainClock.advanceTimeByFrame()
1390         rule.runOnIdle { assertEquals(1968, animatedValue1) }
1391         rule.mainClock.advanceTimeBy(1000)
1392         rule.runOnIdle { assertEquals(0, animatedValue1) }
1393     }
1394 
1395     @Test
1396     fun snapToStopsAllAnimations() {
1397         rule.mainClock.autoAdvance = false
1398         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1399         var animatedValue1 by mutableIntStateOf(-1)
1400         lateinit var coroutineScope: CoroutineScope
1401 
1402         rule.setContent {
1403             coroutineScope = rememberCoroutineScope()
1404             val transition = rememberTransition(seekableTransitionState, label = "Test")
1405             val val1 =
1406                 transition.animateInt(
1407                     label = "Value",
1408                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1409                 ) { state ->
1410                     val target =
1411                         when (state) {
1412                             AnimStates.From -> 0
1413                             AnimStates.Other -> 2000
1414                             else -> 1000
1415                         }
1416                     target
1417                 }
1418             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1419         }
1420         rule.waitForIdle()
1421         rule.runOnUiThread {
1422             coroutineScope.launch { seekableTransitionState.seekTo(1f, AnimStates.To) }
1423         }
1424         rule.mainClock.advanceTimeByFrame()
1425         rule.waitForIdle()
1426         val animation =
1427             rule.runOnUiThread {
1428                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.Other) }
1429             }
1430         rule.mainClock.advanceTimeByFrame()
1431         rule.waitForIdle()
1432         val snapTo =
1433             rule.runOnUiThread {
1434                 coroutineScope.async { seekableTransitionState.snapTo(AnimStates.From) }
1435             }
1436         rule.mainClock.advanceTimeByFrame()
1437         rule.runOnIdle {
1438             assertTrue(animation.isCancelled)
1439             assertTrue(snapTo.isCompleted)
1440             assertEquals(0, animatedValue1)
1441         }
1442     }
1443 
1444     @Test
1445     fun snapToSameTargetState() {
1446         rule.mainClock.autoAdvance = false
1447         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1448         var animatedValue1 by mutableIntStateOf(-1)
1449         lateinit var coroutineScope: CoroutineScope
1450 
1451         rule.setContent {
1452             coroutineScope = rememberCoroutineScope()
1453             val transition = rememberTransition(seekableTransitionState, label = "Test")
1454             val val1 =
1455                 transition.animateInt(
1456                     label = "Value",
1457                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1458                 ) { state ->
1459                     val target =
1460                         when (state) {
1461                             AnimStates.From -> 0
1462                             AnimStates.Other -> 2000
1463                             else -> 1000
1464                         }
1465                     target
1466                 }
1467             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1468         }
1469         rule.waitForIdle()
1470         val seekTo =
1471             rule.runOnUiThread {
1472                 coroutineScope.async { seekableTransitionState.seekTo(0.5f, AnimStates.To) }
1473             }
1474         rule.mainClock.advanceTimeByFrame()
1475         rule.runOnIdle { assertTrue(seekTo.isCompleted) }
1476         val snapTo =
1477             rule.runOnUiThread {
1478                 coroutineScope.async { seekableTransitionState.snapTo(AnimStates.To) }
1479             }
1480         rule.mainClock.advanceTimeByFrame()
1481         rule.runOnIdle {
1482             assertTrue(snapTo.isCompleted)
1483             assertEquals(1000, animatedValue1)
1484         }
1485     }
1486 
1487     @Test
1488     fun snapToSameCurrentState() {
1489         rule.mainClock.autoAdvance = false
1490         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1491         var animatedValue1 by mutableIntStateOf(-1)
1492         lateinit var coroutineScope: CoroutineScope
1493 
1494         rule.setContent {
1495             coroutineScope = rememberCoroutineScope()
1496             val transition = rememberTransition(seekableTransitionState, label = "Test")
1497             val val1 =
1498                 transition.animateInt(
1499                     label = "Value",
1500                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1501                 ) { state ->
1502                     val target =
1503                         when (state) {
1504                             AnimStates.From -> 0
1505                             AnimStates.Other -> 2000
1506                             else -> 1000
1507                         }
1508                     target
1509                 }
1510             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1511         }
1512         rule.waitForIdle()
1513         val seekTo =
1514             rule.runOnUiThread {
1515                 coroutineScope.async { seekableTransitionState.seekTo(0.5f, AnimStates.To) }
1516             }
1517         rule.mainClock.advanceTimeByFrame()
1518         rule.runOnIdle { assertTrue(seekTo.isCompleted) }
1519         val snapTo =
1520             rule.runOnUiThread {
1521                 coroutineScope.async { seekableTransitionState.snapTo(AnimStates.From) }
1522             }
1523         rule.mainClock.advanceTimeByFrame()
1524         rule.runOnIdle {
1525             assertTrue(snapTo.isCompleted)
1526             assertEquals(0, animatedValue1)
1527         }
1528     }
1529 
1530     @Test
1531     fun snapToExistingState() {
1532         rule.mainClock.autoAdvance = false
1533         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1534         var animatedValue1 by mutableIntStateOf(-1)
1535         lateinit var coroutineScope: CoroutineScope
1536 
1537         rule.setContent {
1538             coroutineScope = rememberCoroutineScope()
1539             val transition = rememberTransition(seekableTransitionState, label = "Test")
1540             val val1 =
1541                 transition.animateInt(
1542                     label = "Value",
1543                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1544                 ) { state ->
1545                     val target =
1546                         when (state) {
1547                             AnimStates.From -> 0
1548                             AnimStates.Other -> 2000
1549                             else -> 1000
1550                         }
1551                     target
1552                 }
1553             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1554         }
1555         rule.waitForIdle()
1556         val snapTo =
1557             rule.runOnUiThread {
1558                 coroutineScope.async { seekableTransitionState.snapTo(AnimStates.From) }
1559             }
1560         rule.mainClock.advanceTimeByFrame()
1561         rule.runOnIdle {
1562             assertTrue(snapTo.isCompleted)
1563             assertEquals(0, animatedValue1)
1564         }
1565         val seekAndSnap =
1566             rule.runOnUiThread {
1567                 coroutineScope.async {
1568                     seekableTransitionState.seekTo(0.5f, AnimStates.To)
1569                     seekableTransitionState.snapTo(AnimStates.From)
1570                     seekableTransitionState.snapTo(AnimStates.From)
1571                 }
1572             }
1573         rule.mainClock.advanceTimeByFrame() // seekTo
1574         rule.mainClock.advanceTimeByFrame() // snapTo
1575         rule.runOnIdle {
1576             assertTrue(seekAndSnap.isCompleted)
1577             assertEquals(0, animatedValue1)
1578         }
1579     }
1580 
1581     @Test
1582     fun animateAndContinueAnimation() {
1583         rule.mainClock.autoAdvance = false
1584         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1585         var animatedValue1 by mutableIntStateOf(-1)
1586         lateinit var coroutineScope: CoroutineScope
1587 
1588         rule.setContent {
1589             coroutineScope = rememberCoroutineScope()
1590             val transition = rememberTransition(seekableTransitionState, label = "Test")
1591             val val1 =
1592                 transition.animateInt(
1593                     label = "Value",
1594                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1595                 ) { state ->
1596                     val target =
1597                         when (state) {
1598                             AnimStates.From -> 0
1599                             AnimStates.Other -> 2000
1600                             else -> 1000
1601                         }
1602                     target
1603                 }
1604             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1605         }
1606         rule.waitForIdle()
1607         val seekTo =
1608             rule.runOnUiThread {
1609                 coroutineScope.async {
1610                     seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
1611                 }
1612             }
1613         rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
1614         rule.runOnIdle { assertTrue(seekTo.isCompleted) }
1615         val animateTo =
1616             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
1617         rule.mainClock.advanceTimeByFrame() // lock animation clock
1618         rule.mainClock.advanceTimeBy(160)
1619         rule.runOnIdle { assertEquals(160, animatedValue1) }
1620 
1621         val animateTo2 =
1622             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
1623 
1624         rule.runOnIdle { assertTrue(animateTo.isCancelled) }
1625 
1626         rule.mainClock.advanceTimeByFrame() // continue the animation
1627 
1628         rule.runOnIdle { assertEquals(176, animatedValue1) }
1629 
1630         rule.mainClock.advanceTimeBy(900)
1631 
1632         rule.runOnIdle {
1633             assertTrue(animateTo2.isCompleted)
1634             assertEquals(1000, animatedValue1)
1635         }
1636     }
1637 
1638     @Test
1639     fun continueAnimationWithNewSpec() {
1640         rule.mainClock.autoAdvance = false
1641         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1642         var animatedValue1 by mutableIntStateOf(-1)
1643         lateinit var coroutineScope: CoroutineScope
1644 
1645         rule.setContent {
1646             coroutineScope = rememberCoroutineScope()
1647             val transition = rememberTransition(seekableTransitionState, label = "Test")
1648             val val1 =
1649                 transition.animateInt(
1650                     label = "Value",
1651                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1652                 ) { state ->
1653                     val target =
1654                         when (state) {
1655                             AnimStates.From -> 0
1656                             AnimStates.Other -> 2000
1657                             else -> 1000
1658                         }
1659                     target
1660                 }
1661             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1662         }
1663         rule.waitForIdle()
1664         val seekTo =
1665             rule.runOnUiThread {
1666                 coroutineScope.async {
1667                     seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
1668                 }
1669             }
1670         rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
1671         rule.runOnIdle { assertTrue(seekTo.isCompleted) }
1672         val animateTo =
1673             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
1674         rule.mainClock.advanceTimeByFrame() // lock animation clock
1675         rule.mainClock.advanceTimeBy(160)
1676         rule.runOnIdle { assertEquals(160, animatedValue1) }
1677 
1678         val animateTo2 =
1679             rule.runOnUiThread {
1680                 coroutineScope.async {
1681                     seekableTransitionState.animateTo(
1682                         animationSpec = tween(durationMillis = 200, easing = LinearEasing)
1683                     )
1684                 }
1685             }
1686 
1687         rule.runOnIdle { assertTrue(animateTo.isCancelled) }
1688 
1689         rule.mainClock.advanceTimeByFrame() // continue the animation
1690 
1691         rule.runOnIdle {
1692             // 160 + (840 * 16/200) = 227.2
1693             assertEquals(227.2f, animatedValue1.toFloat(), 1f)
1694         }
1695 
1696         rule.mainClock.advanceTimeBy(200)
1697 
1698         rule.runOnIdle {
1699             assertTrue(animateTo2.isCompleted)
1700             assertEquals(1000, animatedValue1)
1701         }
1702     }
1703 
1704     @Test
1705     fun continueAnimationUsesInitialVelocity() {
1706         rule.mainClock.autoAdvance = false
1707         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1708         var animatedValue1 by mutableIntStateOf(-1)
1709         lateinit var coroutineScope: CoroutineScope
1710 
1711         rule.setContent {
1712             coroutineScope = rememberCoroutineScope()
1713             val transition = rememberTransition(seekableTransitionState, label = "Test")
1714             val val1 =
1715                 transition.animateInt(
1716                     label = "Value",
1717                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
1718                 ) { state ->
1719                     val target =
1720                         when (state) {
1721                             AnimStates.From -> 0
1722                             AnimStates.Other -> 2000
1723                             else -> 1000
1724                         }
1725                     target
1726                 }
1727             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1728         }
1729         rule.waitForIdle()
1730         val seekTo =
1731             rule.runOnUiThread {
1732                 coroutineScope.async {
1733                     seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
1734                 }
1735             }
1736         rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
1737         rule.runOnIdle { assertTrue(seekTo.isCompleted) }
1738         val animateTo =
1739             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.animateTo() } }
1740         rule.mainClock.advanceTimeByFrame() // lock animation clock
1741         rule.mainClock.advanceTimeBy(800) // half way
1742         rule.runOnIdle { assertEquals(500, animatedValue1) }
1743 
1744         rule.runOnUiThread {
1745             coroutineScope.launch {
1746                 seekableTransitionState.animateTo(
1747                     animationSpec =
1748                         spring(visibilityThreshold = 0.01f, stiffness = Spring.StiffnessVeryLow)
1749                 )
1750             }
1751         }
1752 
1753         rule.runOnIdle { assertTrue(animateTo.isCancelled) }
1754 
1755         rule.mainClock.advanceTimeByFrame() // continue the animation
1756 
1757         rule.runOnIdle {
1758             // The velocity should be similar to what it was before after only one frame
1759             // 500 / 800 = 0.625 pixels per ms * 16 = 10 pixels
1760             assertEquals(510f, animatedValue1.toFloat(), 2f)
1761         }
1762     }
1763 
1764     @Test
1765     fun continueAnimationNewSpecUsesInitialVelocity() {
1766         rule.mainClock.autoAdvance = false
1767         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1768         var animatedValue1 by mutableIntStateOf(-1)
1769         lateinit var coroutineScope: CoroutineScope
1770 
1771         rule.setContent {
1772             coroutineScope = rememberCoroutineScope()
1773             val transition = rememberTransition(seekableTransitionState, label = "Test")
1774             val val1 =
1775                 transition.animateInt(
1776                     label = "Value",
1777                     transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) }
1778                 ) { state ->
1779                     val target =
1780                         when (state) {
1781                             AnimStates.From -> 0
1782                             AnimStates.Other -> 2000
1783                             else -> 1000
1784                         }
1785                     target
1786                 }
1787             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1788         }
1789         rule.waitForIdle()
1790         val seekTo =
1791             rule.runOnUiThread {
1792                 coroutineScope.async {
1793                     seekableTransitionState.seekTo(fraction = 0f, targetState = AnimStates.To)
1794                 }
1795             }
1796         rule.mainClock.advanceTimeByFrame() // wait for composition after seekTo
1797         rule.runOnIdle { assertTrue(seekTo.isCompleted) }
1798         val springSpec = spring<Float>(dampingRatio = 2f)
1799         val vecSpringSpec = springSpec.vectorize(Float.VectorConverter)
1800         val animateTo =
1801             rule.runOnUiThread {
1802                 coroutineScope.async {
1803                     seekableTransitionState.animateTo(animationSpec = springSpec)
1804                 }
1805             }
1806         rule.mainClock.advanceTimeByFrame() // lock animation clock
1807 
1808         // find how long it takes to get to about half way:
1809         var halfDuration = 16L
1810         val zeroVector = AnimationVector1D(0f)
1811         val oneVector = AnimationVector1D(1f)
1812         while (
1813             vecSpringSpec
1814                 .getValueFromMillis(
1815                     playTimeMillis = halfDuration,
1816                     start = zeroVector,
1817                     end = oneVector,
1818                     startVelocity = zeroVector
1819                 )[0] < 0.5f
1820         ) {
1821             halfDuration += 16L
1822         }
1823         rule.mainClock.advanceTimeBy(halfDuration) // ~half way
1824         val halfValue =
1825             vecSpringSpec
1826                 .getValueFromMillis(
1827                     playTimeMillis = halfDuration,
1828                     start = zeroVector,
1829                     end = oneVector,
1830                     startVelocity = zeroVector
1831                 )[0] * 1000
1832         rule.runOnIdle { assertEquals(halfValue, animatedValue1.toFloat(), 1f) }
1833 
1834         val velocityAtHalfWay =
1835             vecSpringSpec
1836                 .getVelocityFromNanos(
1837                     playTimeNanos = halfDuration * MillisToNanos,
1838                     initialValue = zeroVector,
1839                     targetValue = oneVector,
1840                     initialVelocity = zeroVector
1841                 )[0]
1842 
1843         rule.runOnUiThread {
1844             coroutineScope.launch {
1845                 seekableTransitionState.animateTo(
1846                     animationSpec =
1847                         spring(
1848                             visibilityThreshold = 0.01f,
1849                             stiffness = Spring.StiffnessVeryLow,
1850                             dampingRatio = Spring.DampingRatioHighBouncy
1851                         )
1852                 )
1853             }
1854         }
1855 
1856         rule.runOnIdle { assertTrue(animateTo.isCancelled) }
1857 
1858         rule.mainClock.advanceTimeByFrame() // continue the animation
1859 
1860         rule.runOnIdle {
1861             // The velocity should be similar to what it was before after only one frame
1862             assertEquals(halfValue + (velocityAtHalfWay * 16f), animatedValue1.toFloat(), 2f)
1863         }
1864     }
1865 
1866     @Test
1867     fun animationCompletionHasNoInitialValueAnimation() {
1868         rule.mainClock.autoAdvance = false
1869         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1870         var animatedValue1 by mutableIntStateOf(-1)
1871         lateinit var coroutineScope: CoroutineScope
1872 
1873         rule.setContent {
1874             coroutineScope = rememberCoroutineScope()
1875             val transition = rememberTransition(seekableTransitionState, label = "Test")
1876             val val1 =
1877                 transition.animateInt(
1878                     label = "Value",
1879                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
1880                 ) { state ->
1881                     val target =
1882                         when (state) {
1883                             AnimStates.From -> 0
1884                             AnimStates.Other -> 2000
1885                             else -> 1000
1886                         }
1887                     target
1888                 }
1889             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
1890         }
1891         rule.waitForIdle()
1892         rule.runOnUiThread {
1893             coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.To) }
1894         }
1895         rule.mainClock.advanceTimeBy(1700)
1896         rule.runOnUiThread {
1897             coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.From) }
1898         }
1899         rule.mainClock.advanceTimeByFrame() // lock in the clock
1900         rule.runOnIdle { assertEquals(1000, animatedValue1) }
1901         rule.mainClock.advanceTimeByFrame()
1902         rule.runOnIdle { assertEquals(990, animatedValue1) }
1903         rule.mainClock.advanceTimeByFrame()
1904         rule.runOnIdle { assertEquals(980, animatedValue1) }
1905     }
1906 
1907     @Test
1908     fun animationDurationWorksOnInitialStateChange() {
1909         rule.mainClock.autoAdvance = false
1910         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1911         var animatedValue1 by mutableIntStateOf(-1)
1912         var animatedValue2 by mutableIntStateOf(-1)
1913         lateinit var coroutineScope: CoroutineScope
1914 
1915         rule.setContent {
1916             coroutineScope = rememberCoroutineScope()
1917             val transition = rememberTransition(seekableTransitionState, label = "Test")
1918             val val1 =
1919                 transition.animateInt(
1920                     label = "Value",
1921                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
1922                 ) { state ->
1923                     when (state) {
1924                         AnimStates.From -> 0
1925                         AnimStates.Other -> 2000
1926                         else -> 1000
1927                     }
1928                 }
1929             val val2 =
1930                 transition.animateInt(
1931                     label = "Value2",
1932                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
1933                 ) { state ->
1934                     when (state) {
1935                         AnimStates.From -> 0
1936                         else -> 1000
1937                     }
1938                 }
1939             Box(
1940                 Modifier.fillMaxSize().drawBehind {
1941                     animatedValue1 = val1.value
1942                     animatedValue2 = val2.value
1943                 }
1944             )
1945         }
1946         rule.waitForIdle()
1947         rule.runOnUiThread {
1948             coroutineScope.launch {
1949                 seekableTransitionState.animateTo(
1950                     AnimStates.To,
1951                     animationSpec = tween(durationMillis = 160, easing = LinearEasing)
1952                 )
1953             }
1954         }
1955         rule.mainClock.advanceTimeByFrame() // lock in the clock
1956         rule.runOnUiThread {
1957             coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.Other) }
1958         }
1959         rule.mainClock.advanceTimeByFrame() // advance one frame toward To and compose to Other
1960         rule.runOnIdle {
1961             assertEquals(100, animatedValue1)
1962             assertEquals(100, animatedValue2)
1963         }
1964         rule.mainClock.advanceTimeByFrame()
1965         rule.runOnIdle {
1966             // 200 + (1800 * 16/1600) = 218
1967             assertEquals(218, animatedValue1)
1968             // continue the animatedValue2 animation
1969             assertEquals(200, animatedValue2)
1970         }
1971         rule.mainClock.advanceTimeBy(128)
1972         rule.runOnIdle {
1973             // 1000 + (1000 * 144/1600) = 1090
1974             assertEquals(1090, animatedValue1)
1975             assertEquals(1000, animatedValue2)
1976         }
1977         rule.mainClock.advanceTimeBy(1600)
1978         rule.runOnIdle {
1979             assertEquals(2000, animatedValue1)
1980             assertEquals(1000, animatedValue2)
1981         }
1982     }
1983 
1984     @Test
1985     fun animationAlreadyAtTarget() {
1986         rule.mainClock.autoAdvance = false
1987         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
1988         var animatedValue1 by mutableIntStateOf(-1)
1989         lateinit var coroutineScope: CoroutineScope
1990 
1991         rule.setContent {
1992             coroutineScope = rememberCoroutineScope()
1993             val transition = rememberTransition(seekableTransitionState, label = "Test")
1994             val val1 =
1995                 transition.animateInt(
1996                     label = "Value",
1997                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
1998                 ) { state ->
1999                     when (state) {
2000                         AnimStates.From -> 0
2001                         else -> 1000
2002                     }
2003                 }
2004             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
2005         }
2006         rule.waitForIdle()
2007         val seekTo =
2008             rule.runOnUiThread {
2009                 coroutineScope.async { seekableTransitionState.seekTo(1f, AnimStates.To) }
2010             }
2011         rule.mainClock.advanceTimeByFrame() // wait for composition
2012         rule.runOnIdle {
2013             assertTrue(seekTo.isCompleted)
2014             assertEquals(1000, animatedValue1)
2015         }
2016         val anim =
2017             rule.runOnUiThread {
2018                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.To) }
2019             }
2020         rule.mainClock.advanceTimeByFrame() // compose to current state = target state
2021         rule.runOnIdle {
2022             assertTrue(anim.isCompleted)
2023             assertEquals(AnimStates.To, seekableTransitionState.currentState)
2024             assertEquals(AnimStates.To, seekableTransitionState.targetState)
2025         }
2026     }
2027 
2028     @Test
2029     fun seekCurrentEqualsTarget() {
2030         rule.mainClock.autoAdvance = false
2031         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2032         var animatedValue1 by mutableIntStateOf(-1)
2033         lateinit var coroutineScope: CoroutineScope
2034 
2035         rule.setContent {
2036             coroutineScope = rememberCoroutineScope()
2037             val transition = rememberTransition(seekableTransitionState, label = "Test")
2038             val val1 =
2039                 transition.animateInt(
2040                     label = "Value",
2041                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
2042                 ) { state ->
2043                     when (state) {
2044                         AnimStates.From -> 0
2045                         else -> 1000
2046                     }
2047                 }
2048             Box(Modifier.fillMaxSize().drawBehind { animatedValue1 = val1.value })
2049         }
2050         rule.waitForIdle()
2051         val seekTo =
2052             rule.runOnUiThread { coroutineScope.async { seekableTransitionState.seekTo(0.5f) } }
2053         rule.runOnIdle {
2054             assertTrue(seekTo.isCompleted)
2055             assertEquals(0, animatedValue1)
2056         }
2057     }
2058 
2059     @Test
2060     fun animateToAtEndCurrentInitialValueAnimations() {
2061         rule.mainClock.autoAdvance = false
2062         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2063         var animatedValue1 by mutableIntStateOf(-1)
2064         var animatedValue2 by mutableIntStateOf(-1)
2065         lateinit var coroutineScope: CoroutineScope
2066 
2067         rule.setContent {
2068             coroutineScope = rememberCoroutineScope()
2069             val transition = rememberTransition(seekableTransitionState, label = "Test")
2070             val val1 =
2071                 transition.animateInt(
2072                     label = "Value",
2073                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
2074                 ) { state ->
2075                     when (state) {
2076                         AnimStates.From -> 0
2077                         AnimStates.To -> 1000
2078                         AnimStates.Other -> 2000
2079                     }
2080                 }
2081             val val2 =
2082                 transition.animateInt(
2083                     label = "Value",
2084                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
2085                 ) { state ->
2086                     when (state) {
2087                         AnimStates.From -> 0
2088                         else -> 1000
2089                     }
2090                 }
2091             Box(
2092                 Modifier.fillMaxSize().drawBehind {
2093                     animatedValue1 = val1.value
2094                     animatedValue2 = val2.value
2095                 }
2096             )
2097         }
2098         rule.waitForIdle()
2099         val seekTo =
2100             rule.runOnUiThread {
2101                 coroutineScope.async { seekableTransitionState.seekTo(0f, AnimStates.To) }
2102             }
2103         rule.mainClock.advanceTimeByFrame() // compose to To
2104         assertTrue(seekTo.isCompleted)
2105         val seekOther =
2106             rule.runOnUiThread {
2107                 coroutineScope.async { seekableTransitionState.seekTo(1f, AnimStates.Other) }
2108             }
2109         rule.mainClock.advanceTimeByFrame() // compose to Other
2110         assertFalse(seekOther.isCompleted) // should be animating animatedValue2
2111         val animateOther =
2112             rule.runOnUiThread {
2113                 coroutineScope.async {
2114                     // already at the end (1f), but it should continue the animatedValue2 animation
2115                     seekableTransitionState.animateTo(AnimStates.Other)
2116                 }
2117             }
2118         assertTrue(seekOther.isCancelled)
2119         assertTrue(animateOther.isActive)
2120         rule.mainClock.advanceTimeByFrame() // advance the animation
2121         rule.runOnIdle {
2122             assertTrue(animateOther.isActive)
2123             assertEquals(2000, animatedValue1)
2124             assertEquals(1000 * 16 / 1600, animatedValue2)
2125         }
2126     }
2127 
2128     @Test
2129     fun changingDuration() {
2130         rule.mainClock.autoAdvance = false
2131         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2132         var animatedValue1 by mutableIntStateOf(-1)
2133         var animatedValue2 by mutableIntStateOf(-1)
2134         lateinit var coroutineScope: CoroutineScope
2135 
2136         rule.setContent {
2137             coroutineScope = rememberCoroutineScope()
2138             val transition = rememberTransition(seekableTransitionState, label = "Test")
2139             val val1 =
2140                 transition.animateInt(
2141                     label = "Value",
2142                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
2143                 ) { state ->
2144                     when (state) {
2145                         AnimStates.From -> 0
2146                         else -> 1000
2147                     }
2148                 }
2149             val val2 =
2150                 if (val1.value < 500) {
2151                     mutableFloatStateOf(0f)
2152                 } else {
2153                     transition.animateFloat(
2154                         label = "Value2",
2155                         transitionSpec = { tween(durationMillis = 3200, easing = LinearEasing) }
2156                     ) { state ->
2157                         when (state) {
2158                             AnimStates.From -> 0f
2159                             else -> 1000f
2160                         }
2161                     }
2162                 }
2163             Box(
2164                 Modifier.fillMaxSize().drawBehind {
2165                     animatedValue1 = val1.value
2166                     animatedValue2 = val2.value.roundToInt()
2167                 }
2168             )
2169         }
2170         rule.waitForIdle()
2171         val anim =
2172             rule.runOnUiThread {
2173                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.To) }
2174             }
2175         rule.mainClock.advanceTimeByFrame() // wait for composition
2176         rule.mainClock.advanceTimeBy(800) // half way through
2177         rule.runOnIdle {
2178             assertEquals(500, animatedValue1)
2179             assertEquals(0, animatedValue2)
2180         }
2181         rule.mainClock.advanceTimeByFrame()
2182         rule.runOnIdle {
2183             assertEquals(510, animatedValue1)
2184             assertEquals(5, animatedValue2)
2185         }
2186         rule.mainClock.advanceTimeBy(784)
2187         rule.runOnIdle {
2188             assertEquals(1000, animatedValue1)
2189             assertEquals(250, animatedValue2)
2190             assertFalse(anim.isCompleted)
2191         }
2192         rule.mainClock.advanceTimeBy(2400)
2193         rule.runOnIdle {
2194             assertEquals(1000, animatedValue1)
2195             assertEquals(1000, animatedValue2)
2196         }
2197         rule.mainClock.advanceTimeByFrame() // wait for composition
2198         assertTrue(anim.isCompleted)
2199     }
2200 
2201     @Test
2202     fun changingAnimationWithAnimateToThirdState() {
2203         rule.mainClock.autoAdvance = false
2204         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2205         var animatedValue1 by mutableIntStateOf(-1)
2206         var animatedValue2 by mutableFloatStateOf(-1f)
2207         lateinit var coroutineScope: CoroutineScope
2208 
2209         rule.setContent {
2210             coroutineScope = rememberCoroutineScope()
2211             val transition = rememberTransition(seekableTransitionState, label = "Test")
2212             val val1 =
2213                 transition.animateInt(
2214                     label = "Value",
2215                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
2216                 ) { state ->
2217                     when (state) {
2218                         AnimStates.From -> 0
2219                         AnimStates.To -> 1000
2220                         else -> 2000
2221                     }
2222                 }
2223             val val2 =
2224                 if (val1.value < 500) {
2225                     mutableFloatStateOf(0f)
2226                 } else {
2227                     transition.animateFloat(
2228                         label = "Value2",
2229                         transitionSpec = { tween(durationMillis = 3200, easing = LinearEasing) }
2230                     ) { state ->
2231                         when (state) {
2232                             AnimStates.From -> 0f
2233                             AnimStates.To -> 1000f
2234                             else -> 2000f
2235                         }
2236                     }
2237                 }
2238             Box(
2239                 Modifier.fillMaxSize().drawBehind {
2240                     animatedValue1 = val1.value
2241                     animatedValue2 = val2.value
2242                 }
2243             )
2244         }
2245         rule.waitForIdle()
2246         val animateTo =
2247             rule.runOnUiThread {
2248                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.To) }
2249             }
2250         rule.mainClock.advanceTimeByFrame() // wait for composition
2251         rule.mainClock.advanceTimeBy(800) // half way through
2252 
2253         rule.runOnIdle {
2254             // Won't have advanced the value to Other, but will continue advance to To
2255             assertEquals(1000 * 800 / 1600, animatedValue1)
2256             assertEquals(1000 * 0 / 3200, animatedValue2.roundToInt())
2257         }
2258         rule.mainClock.advanceTimeByFrame() // one frame past recomposition, so animation is running
2259 
2260         rule.runOnIdle {
2261             // Won't have advanced the value to Other, but will continue advance to To
2262             assertEquals(1000 * 816 / 1600, animatedValue1)
2263             assertEquals(1000 * 16 / 3200, animatedValue2.roundToInt())
2264         }
2265 
2266         // now seek to third state
2267         val animateOther =
2268             rule.runOnUiThread {
2269                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.Other) }
2270             }
2271         assertTrue(animateTo.isCancelled)
2272         rule.mainClock.advanceTimeByFrame() // wait for composition
2273         rule.runOnIdle {
2274             // Won't have advanced the value to Other, but will continue advance to To
2275             assertEquals(1000 * 832 / 1600, animatedValue1)
2276             assertEquals(1000 * 32 / 3200, animatedValue2.roundToInt())
2277         }
2278         rule.mainClock.advanceTimeByFrame()
2279         rule.runOnIdle {
2280             // Continues the advance to Other
2281             val anim1Value1 = 1000 * 848 / 1600
2282             val anim1Value2 = 1000 * 48 / 3200
2283             assertEquals(anim1Value1 + ((2000 - anim1Value1) * 16 / 1600), animatedValue1)
2284             assertEquals(anim1Value2 + ((2000f - anim1Value2) * 16 / 3200), animatedValue2)
2285         }
2286 
2287         rule.mainClock.advanceTimeBy(752)
2288         rule.runOnIdle {
2289             val anim1Value1 = 1000
2290             val anim1Value2 = 1000 * 800 / 3200
2291             assertEquals(anim1Value1 + ((2000 - anim1Value1) * 768 / 1600), animatedValue1)
2292             assertEquals(anim1Value2 + ((2000f - anim1Value2) * 768 / 3200), animatedValue2)
2293             assertFalse(animateOther.isCompleted)
2294         }
2295 
2296         rule.mainClock.advanceTimeBy(832)
2297         rule.runOnIdle {
2298             val anim1Value2 = 1000 * 1632 / 3200
2299             assertEquals(2000, animatedValue1)
2300             assertEquals(anim1Value2 + ((2000f - anim1Value2) * 1600 / 3200), animatedValue2)
2301             assertFalse(animateOther.isCompleted)
2302         }
2303 
2304         rule.mainClock.advanceTimeBy(1600)
2305         rule.runOnIdle {
2306             assertEquals(2000, animatedValue1)
2307             assertEquals(2000f, animatedValue2)
2308             assertFalse(animateOther.isCompleted)
2309         }
2310 
2311         rule.mainClock.advanceTimeByFrame() // composition after the current value changes
2312         rule.runOnIdle { assertTrue(animateOther.isCompleted) }
2313     }
2314 
2315     @SdkSuppress(minSdkVersion = 26)
2316     @OptIn(ExperimentalAnimationApi::class)
2317     @Test
2318     fun animateAfterSeekToZero() {
2319         rule.mainClock.autoAdvance = false
2320         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2321         var animatedValue1 by mutableIntStateOf(-1)
2322         lateinit var coroutineScope: CoroutineScope
2323 
2324         rule.setContent {
2325             coroutineScope = rememberCoroutineScope()
2326             val transition = rememberTransition(seekableTransitionState, label = "Test")
2327             val val1 =
2328                 transition.animateInt(
2329                     label = "Value",
2330                     transitionSpec = { tween(durationMillis = 1600, easing = LinearEasing) }
2331                 ) { state ->
2332                     when (state) {
2333                         AnimStates.From -> 0
2334                         else -> 1000
2335                     }
2336                 }
2337 
2338             CompositionLocalProvider(LocalDensity provides Density(1f)) {
2339                 Box(
2340                     Modifier.requiredSize(100.dp).testTag("AV_parent").drawBehind {
2341                         animatedValue1 = val1.value
2342                         drawRect(Color.White)
2343                     }
2344                 ) {
2345                     transition.AnimatedVisibility({ it == AnimStates.To }) {
2346                         Box(Modifier.fillMaxSize().background(Color.Red))
2347                     }
2348                 }
2349             }
2350         }
2351         rule.waitForIdle()
2352         val initialAnimateAndSeek =
2353             rule.runOnUiThread {
2354                 coroutineScope.async {
2355                     seekableTransitionState.animateTo(AnimStates.To)
2356                     seekableTransitionState.seekTo(0.5f, targetState = AnimStates.From)
2357                     seekableTransitionState.seekTo(0f, targetState = AnimStates.From)
2358                 }
2359             }
2360         rule.mainClock.advanceTimeBy(5000)
2361         rule.runOnIdle {
2362             assertTrue(initialAnimateAndSeek.isCompleted)
2363             assertEquals(1000, animatedValue1)
2364         }
2365         rule.onNodeWithTag("AV_parent").run {
2366             assertExists("Error: Node doesn't exist")
2367             captureToImage().run {
2368                 assertEquals(100, width)
2369                 assertEquals(100, height)
2370                 assertPixels { _ -> Color.Red }
2371             }
2372         }
2373         val secondAnimate =
2374             rule.runOnUiThread {
2375                 coroutineScope.async { seekableTransitionState.animateTo(AnimStates.To) }
2376             }
2377         rule.waitForIdle()
2378         // This waits for the initial state animation to finish, since we changed the initial state
2379         // when going from seeking to animating.
2380         rule.mainClock.advanceTimeBy(5000)
2381         rule.runOnIdle {
2382             assertTrue(secondAnimate.isCompleted)
2383             assertEquals(1000, animatedValue1)
2384         }
2385         rule.onNodeWithTag("AV_parent").run {
2386             assertExists("Error: Node doesn't exist")
2387             captureToImage().run {
2388                 assertEquals(100, width)
2389                 assertEquals(100, height)
2390                 assertPixels { _ -> Color.Red }
2391             }
2392         }
2393     }
2394 
2395     @Test
2396     fun isRunningDuringAnimateTo() {
2397         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2398         lateinit var transition: Transition<AnimStates>
2399         var animatedValue by mutableIntStateOf(-1)
2400 
2401         rule.mainClock.autoAdvance = false
2402 
2403         rule.setContent {
2404             LaunchedEffect(seekableTransitionState) {
2405                 seekableTransitionState.animateTo(AnimStates.To)
2406             }
2407             transition = rememberTransition(seekableTransitionState, label = "Test")
2408             animatedValue =
2409                 transition
2410                     .animateInt(
2411                         label = "Value",
2412                         transitionSpec = { tween(easing = LinearEasing) }
2413                     ) { state ->
2414                         when (state) {
2415                             AnimStates.From -> 0
2416                             else -> 1000
2417                         }
2418                     }
2419                     .value
2420         }
2421         rule.runOnIdle {
2422             assertEquals(0, animatedValue)
2423             assertFalse(transition.isRunning)
2424         }
2425         rule.mainClock.advanceTimeByFrame() // wait for composition after animateTo()
2426         rule.mainClock.advanceTimeByFrame() // one frame to set the start time
2427         rule.runOnIdle {
2428             assertTrue(animatedValue > 0)
2429             assertTrue(transition.isRunning)
2430         }
2431         rule.mainClock.advanceTimeBy(5000)
2432         rule.runOnIdle {
2433             assertEquals(1000, animatedValue)
2434             assertFalse(transition.isRunning)
2435         }
2436     }
2437 
2438     @Test
2439     fun isRunningFalseAfterSnapTo() {
2440         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2441         lateinit var transition: Transition<AnimStates>
2442         var animatedValue by mutableIntStateOf(-1)
2443 
2444         rule.mainClock.autoAdvance = false
2445 
2446         rule.setContent {
2447             LaunchedEffect(seekableTransitionState) {
2448                 awaitFrame() // Not sure why this is needed. Animated val doesn't change without it.
2449                 seekableTransitionState.snapTo(AnimStates.To)
2450             }
2451             transition = rememberTransition(seekableTransitionState, label = "Test")
2452             animatedValue =
2453                 transition
2454                     .animateInt(
2455                         label = "Value",
2456                         transitionSpec = { tween(easing = LinearEasing) }
2457                     ) { state ->
2458                         when (state) {
2459                             AnimStates.From -> 0
2460                             else -> 1000
2461                         }
2462                     }
2463                     .value
2464         }
2465         rule.runOnIdle {
2466             assertEquals(0, animatedValue)
2467             assertFalse(transition.isRunning)
2468         }
2469         rule.mainClock.advanceTimeByFrame() // wait for composition after animateTo()
2470         rule.mainClock.advanceTimeByFrame() // one frame to snap
2471         rule.mainClock.advanceTimeByFrame() // one frame for LaunchedEffect's awaitFrame()
2472         rule.runOnIdle {
2473             assertEquals(1000, animatedValue)
2474             assertFalse(transition.isRunning)
2475         }
2476     }
2477 
2478     @Test
2479     fun isRunningFalseAfterChildAnimatedVisibilityTransition() {
2480         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2481         lateinit var coroutineScope: CoroutineScope
2482         lateinit var transition: Transition<AnimStates>
2483         var animatedVisibilityTransition: Transition<*>? = null
2484 
2485         rule.mainClock.autoAdvance = false
2486 
2487         rule.setContent {
2488             coroutineScope = rememberCoroutineScope()
2489             transition = rememberTransition(seekableTransitionState, label = "Test")
2490             transition.AnimatedVisibility(
2491                 visible = { it == AnimStates.To },
2492             ) {
2493                 animatedVisibilityTransition = this.transition
2494                 Box(Modifier.size(100.dp))
2495             }
2496         }
2497         rule.runOnIdle {
2498             assertFalse(transition.isRunning)
2499             assertNull(animatedVisibilityTransition)
2500         }
2501 
2502         rule.runOnUiThread {
2503             coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.To) }
2504         }
2505         rule.mainClock.advanceTimeBy(50)
2506         rule.runOnIdle {
2507             assertTrue(transition.isRunning)
2508             assertTrue(animatedVisibilityTransition!!.isRunning)
2509         }
2510 
2511         rule.mainClock.advanceTimeBy(5000)
2512         rule.runOnIdle {
2513             assertFalse(transition.isRunning)
2514             assertFalse(animatedVisibilityTransition!!.isRunning)
2515         }
2516     }
2517 
2518     @Test
2519     fun isRunningFalseAfterRemovingAnimationWhileAnimatingToPreviousState() {
2520         val seekableTransitionState = SeekableTransitionState(AnimStates.From)
2521         lateinit var coroutineScope: CoroutineScope
2522         lateinit var transition: Transition<AnimStates>
2523         var floatAnim: State<Float>? = null
2524         var conditionalAnim: State<Float>? = null
2525         var addConditionalAnim by mutableStateOf(true)
2526         rule.setContent {
2527             coroutineScope = rememberCoroutineScope()
2528             transition = rememberTransition(seekableTransitionState)
2529             floatAnim =
2530                 transition.animateFloat(transitionSpec = { tween(500) }) {
2531                     if (it == AnimStates.From) 0f else 1000f
2532                 }
2533             conditionalAnim =
2534                 if (addConditionalAnim) {
2535                     // Longer duration than floatAnim so we can check if it keeps the transition
2536                     // running
2537                     transition.animateFloat(transitionSpec = { tween(10000000) }) {
2538                         if (it == AnimStates.From) 0f else 1000f
2539                     }
2540                 } else {
2541                     null
2542                 }
2543         }
2544         rule.waitForIdle()
2545 
2546         // Check initial values
2547         assertEquals(0f, floatAnim?.value)
2548         assertEquals(0f, conditionalAnim?.value)
2549 
2550         rule.mainClock.autoAdvance = false
2551 
2552         // Animate
2553         rule.runOnUiThread {
2554             coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.To) }
2555         }
2556         rule.mainClock.advanceTimeByFrame()
2557 
2558         // Finish floatAnim but not conditionalAnim
2559         rule.mainClock.advanceTimeBy(750)
2560 
2561         assertEquals(1000f, floatAnim?.value)
2562         assertTrue(conditionalAnim!!.value < 1000f)
2563         assertTrue(transition.isRunning)
2564 
2565         // Animate back
2566         rule.runOnUiThread {
2567             coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.From) }
2568         }
2569         rule.mainClock.advanceTimeByFrame()
2570 
2571         // Finish floatAnim but not conditionalAnim
2572         rule.mainClock.advanceTimeBy(500)
2573 
2574         assertEquals(0f, floatAnim?.value)
2575         assertTrue(conditionalAnim!!.value > 0f)
2576         assertTrue(transition.isRunning)
2577 
2578         // Remove conditionalAnim
2579         addConditionalAnim = false
2580         rule.mainClock.advanceTimeByFrame()
2581         rule.mainClock.advanceTimeByFrame()
2582 
2583         assertTrue(conditionalAnim == null)
2584         assertFalse(transition.isRunning)
2585     }
2586 
2587     @Test
2588     fun testCleanupAfterDispose() {
2589         fun isObserving(): Boolean {
2590             var active = false
2591             SeekableStateObserver.clearIf {
2592                 active = true
2593                 false
2594             }
2595             return active
2596         }
2597 
2598         var seekableState: SeekableTransitionState<*>?
2599         var disposed by mutableStateOf(false)
2600 
2601         rule.setContent {
2602             seekableState = remember { SeekableTransitionState(true) }
2603 
2604             if (!disposed) {
2605                 rememberTransition(transitionState = seekableState!!)
2606             }
2607         }
2608         rule.waitForIdle()
2609         assertTrue(isObserving())
2610 
2611         disposed = true
2612         rule.waitForIdle()
2613         assertFalse(isObserving())
2614     }
2615 
2616     @OptIn(ExperimentalTransitionApi::class)
2617     @Test
2618     fun quickAddAndRemove() {
2619         @Stable
2620         class ScreenState(
2621             val label: String,
2622             removing: Boolean = false,
2623         ) {
2624             var removing by mutableStateOf(removing)
2625         }
2626 
2627         var labelIndex = 1
2628         val screenStates = mutableStateListOf(ScreenState("1"))
2629         val seekableScreenTransitionState = SeekableTransitionState(screenStates.toList())
2630 
2631         rule.setContent {
2632             val screenTransition = rememberTransition(seekableScreenTransitionState)
2633             LaunchedEffect(Unit) {
2634                 snapshotFlow { screenStates.toList().filter { !it.removing } }
2635                     .collectLatest { capturedScreenStates ->
2636                         seekableScreenTransitionState.animateTo(capturedScreenStates)
2637                         // Done animating
2638                         screenStates.fastForEachReversed {
2639                             if (it.removing) {
2640                                 screenStates.remove(it)
2641                             }
2642                         }
2643                     }
2644             }
2645 
2646             Column(Modifier.fillMaxSize()) {
2647                 Box(Modifier.fillMaxWidth().weight(1f)) {
2648                     var lastVisibleIndex = screenStates.size - 1
2649                     while (lastVisibleIndex >= 0 && screenStates[lastVisibleIndex].removing) {
2650                         lastVisibleIndex--
2651                     }
2652 
2653                     screenStates.forEach { screenState ->
2654                         key(screenState) {
2655                             val visibleTransition =
2656                                 screenTransition.createChildTransition {
2657                                     screenState === it.lastOrNull() && !screenState.removing
2658                                 }
2659                             visibleTransition.AnimatedVisibility(
2660                                 visible = { it },
2661                             ) {
2662                                 Text(
2663                                     "Hello ${screenState.label}",
2664                                     Modifier.testTag(screenState.label)
2665                                 )
2666                             }
2667                         }
2668                     }
2669                     Text(
2670                         "screenStates:\n${
2671                             screenStates.reversed().joinToString("\n") {
2672                                 it.label +
2673                                     if (it.removing) " (removing)" else ""
2674                             }
2675                         }",
2676                         Modifier.align(Alignment.BottomStart)
2677                     )
2678                 }
2679             }
2680         }
2681         fun removeState() {
2682             rule.runOnUiThread { screenStates.last { !it.removing }.removing = true }
2683         }
2684         fun addState() {
2685             rule.runOnUiThread { screenStates += ScreenState(label = "${++labelIndex}") }
2686         }
2687 
2688         rule.waitForIdle()
2689         rule.mainClock.autoAdvance = false
2690         addState()
2691         rule.mainClock.advanceTimeBy(50)
2692         removeState()
2693         rule.mainClock.advanceTimeBy(50)
2694         addState()
2695         rule.mainClock.advanceTimeBy(50)
2696         removeState()
2697         rule.mainClock.autoAdvance = true
2698         rule.waitForIdle()
2699 
2700         rule.onNodeWithTag("1").assertIsDisplayed()
2701         rule.onNodeWithTag("2").assertIsNotDisplayed()
2702         rule.onNodeWithTag("3").assertIsNotDisplayed()
2703     }
2704 }
2705