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