[go: nahoru, domu]

Add support for rememberInfiniteTransition in tooling

Test: Unit tests added
Bug: 261569436
Change-Id: I1cbb4a498f020d0e0526fad4fdb0ee21c5c52ebc
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt
index a8eeb79..7855edf 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt
@@ -170,12 +170,16 @@
     @Suppress("ComposableNaming")
     @Composable
     internal fun run() {
+        val toolingOverride = remember {
+            mutableStateOf<State<Long>?>(null)
+        }
         if (isRunning || refreshChildNeeded) {
             LaunchedEffect(this) {
                 var durationScale = 1f
                 // Restart every time duration scale changes
                 while (true) {
                     withInfiniteAnimationFrameNanos {
+                        val currentTimeNanos = toolingOverride.value?.value ?: it
                         if (startTimeNanos == AnimationConstants.UnspecifiedTime ||
                             durationScale != coroutineContext.durationScale
                         ) {
@@ -191,7 +195,8 @@
                                 it.skipToEnd()
                             }
                         } else {
-                            val playTimeNanos = ((it - startTimeNanos) / durationScale).toLong()
+                            val playTimeNanos = ((currentTimeNanos - startTimeNanos) /
+                                durationScale).toLong()
                             onFrame(playTimeNanos)
                         }
                     }
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index 05afe97..cadde62 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -207,10 +207,10 @@
                 "animateContentSize",
                 "TargetBasedAnimation",
                 "DecayAnimation",
-                "InfiniteTransition"
             ),
             transitions = listOf("checkBoxAnim", "Crossfade"),
-            animateXAsState = emptyList()
+            animateXAsState = emptyList(),
+            infiniteTransitions = listOf("InfiniteTransition")
         )
         AnimateXAsStateComposeAnimation.testOverrideAvailability(true)
     }
@@ -249,8 +249,11 @@
     }
 
     @Test
-    fun infiniteTransitionIsNotSubscribed() {
-        checkAnimationsAreSubscribed("InfiniteTransitionPreview")
+    fun infiniteTransitionIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "InfiniteTransitionPreview",
+            infiniteTransitions = listOf("InfiniteTransition")
+        )
     }
 
     @Test
@@ -275,8 +278,8 @@
     fun infiniteAndTransitionIsSubscribed() {
         checkAnimationsAreSubscribed(
             "InfiniteAndTransitionPreview",
-            listOf("InfiniteTransition"),
-            listOf("checkBoxAnim")
+            transitions = listOf("checkBoxAnim"),
+            infiniteTransitions = listOf("InfiniteTransition")
         )
     }
 
@@ -287,7 +290,8 @@
             "AllAnimations",
             emptyList(),
             listOf("checkBoxAnim", "Crossfade"),
-            animateXAsState = listOf("DpAnimation", "IntAnimation")
+            animateXAsState = listOf("DpAnimation", "IntAnimation"),
+            infiniteTransitions = listOf("InfiniteTransition")
         )
         UnsupportedComposeAnimation.testOverrideAvailability(true)
     }
@@ -315,7 +319,8 @@
         preview: String,
         unsupported: List<String> = emptyList(),
         transitions: List<String> = emptyList(),
-        animateXAsState: List<String> = emptyList()
+        animateXAsState: List<String> = emptyList(),
+        infiniteTransitions: List<String> = emptyList()
     ) {
         val clock = PreviewAnimationClock()
 
@@ -329,6 +334,7 @@
             assertTrue(clock.transitionClocks.isEmpty())
             assertTrue(clock.trackedUnsupportedAnimations.isEmpty())
             assertTrue(clock.animatedVisibilityClocks.isEmpty())
+            assertTrue(clock.infiniteTransitionClocks.isEmpty())
         }
 
         waitFor(5, TimeUnit.SECONDS) {
@@ -343,6 +349,8 @@
             assertEquals(transitions, clock.transitionClocks.values.map { it.animation.label })
             assertEquals(animateXAsState,
                 clock.animateXAsStateClocks.values.map { it.animation.label })
+            assertEquals(infiniteTransitions,
+                clock.infiniteTransitionClocks.values.map { it.animation.label })
             assertEquals(0, clock.animatedVisibilityClocks.size)
         }
     }
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
index 2376088..acab459 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.tooling.animation
 
 import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.tooling.ComposeAnimationType
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.tooling.AnimateAsStatePreview
 import androidx.compose.ui.tooling.AnimateAsStateWithLabelsPreview
@@ -31,6 +33,7 @@
 import androidx.compose.ui.tooling.TargetBasedAnimationPreview
 import androidx.compose.ui.tooling.TransitionAnimatedVisibilityPreview
 import androidx.compose.ui.tooling.TransitionPreview
+import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation.Companion.parse
 import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -78,6 +81,25 @@
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
+        val composeAnimation = search.animations.first().parse()!!
+        Assert.assertNotNull(composeAnimation)
+        Assert.assertNotNull(composeAnimation.animationObject)
+        Assert.assertNotNull(composeAnimation.label)
+        assertEquals(1, composeAnimation.states.size)
+        assertEquals(ComposeAnimationType.INFINITE_TRANSITION, composeAnimation.type)
+    }
+
+    @Test
+    fun multipleInfiniteTransitionIsFound() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            rememberInfiniteTransition()
+            rememberInfiniteTransition()
+            rememberInfiniteTransition()
+            rememberInfiniteTransition()
+            rememberInfiniteTransition()
+        }
+        assertEquals(5, search.animations.size)
     }
 
     @Test
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt
new file mode 100644
index 0000000..90d7752
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimationTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.tooling.ComposeAnimationType
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation.Companion.parse
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class InfiniteTransitionComposeAnimationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun apiAvailable() {
+        assertTrue(InfiniteTransitionComposeAnimation.apiAvailable)
+        rule.setContent {
+            val composeAnimation = AnimationSearch.InfiniteTransitionSearchInfo(
+                rememberInfiniteTransition(),
+                ToolingState(0L)
+            ).parse()
+            assertNotNull(composeAnimation)
+            composeAnimation!!
+            assertNotNull(composeAnimation.animationObject)
+            assertNotNull(composeAnimation.label)
+            assertEquals(1, composeAnimation.states.size)
+            assertEquals(ComposeAnimationType.INFINITE_TRANSITION, composeAnimation.type)
+        }
+    }
+
+    @Test
+    fun apiIsNotAvailable() {
+        InfiniteTransitionComposeAnimation.testOverrideAvailability(false)
+        assertFalse(InfiniteTransitionComposeAnimation.apiAvailable)
+        rule.setContent {
+            val composeAnimation = AnimationSearch.InfiniteTransitionSearchInfo(
+                rememberInfiniteTransition(),
+                ToolingState(0L)
+            ).parse()
+            assertNull(composeAnimation)
+        }
+        InfiniteTransitionComposeAnimation.testOverrideAvailability(true)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
index cf3e0b1..2b6de36 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
@@ -23,10 +23,13 @@
 import androidx.compose.animation.core.ExperimentalTransitionApi
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
 import androidx.compose.animation.core.Transition
 import androidx.compose.animation.core.animateDp
 import androidx.compose.animation.core.animateFloat
 import androidx.compose.animation.core.createChildTransition
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.updateTransition
 import androidx.compose.animation.fadeIn
@@ -40,6 +43,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.Utils.searchAndTrackAllAnimations
 import androidx.compose.ui.tooling.animation.states.AnimatedVisibilityState
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
@@ -498,6 +502,50 @@
         assertEquals("AnimatedVisibility", animatedVisibilityImplicitLabel.label)
     }
 
+    @Test
+    fun clockWithInfiniteTransition() {
+        val search = AnimationSearch({ testClock }) {}
+        composeRule.searchAndTrackAllAnimations(search) {
+            // Transition with duration 1000
+            val transition = updateTransition(targetState = 10, label = "updateTransition")
+            transition.animateDp(
+                transitionSpec = { tween(durationMillis = 1000, easing = LinearEasing) },
+                label = "AnimatedDp"
+            ) {
+                if (it == 0) 0.dp else 1.dp
+            }
+            // Infinite transition with duration 300
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                initialValue = 0f,
+                targetValue = 1f,
+                animationSpec = infiniteRepeatable(
+                    tween(300),
+                    RepeatMode.Restart
+                )
+            )
+        }
+        // Default states.
+        assertEquals(300, testClock.getMaxDuration())
+        assertEquals(300, testClock.getMaxDurationPerIteration())
+        val transitionAnimation = testClock.transitionClocks.keys.first()
+        val infiniteAnimation = testClock.infiniteTransitionClocks.keys.first()
+        assertTrue(testClock.getAnimatedProperties(infiniteAnimation).isNotEmpty())
+        testClock.getTransitions(infiniteAnimation, 100).let {
+            assertTrue(it.isNotEmpty())
+            assertTrue(it.first().endTimeMillis <= 500)
+        }
+        // With updated transition state.
+        testClock.updateFromAndToStates(transitionAnimation, 0, 1)
+        composeRule.waitForIdle()
+        assertEquals(1000, testClock.getMaxDuration())
+        assertEquals(1000, testClock.getMaxDurationPerIteration())
+        assertTrue(testClock.getAnimatedProperties(infiniteAnimation).isNotEmpty())
+        val transitions = testClock.getTransitions(infiniteAnimation, 100)
+        assertTrue(transitions.isNotEmpty())
+        assertTrue(transitions.first().endTimeMillis >= 500)
+    }
+
     // Sets up a transition animation scenario, going from RotationColor.RC1 to RotationColor.RC3.
     @Suppress("UNCHECKED_CAST")
     @Composable
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
index 6015f60..78d8fd0 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
@@ -42,7 +42,7 @@
 
     val nullableFloatConverter = TwoWayConverter<Float?, AnimationVector1D>({
         AnimationVector1D(it ?: 0f)
-    }, { it.value })
+    }, { if (it.value == 0f) null else it.value })
 
     val stringConverter = TwoWayConverter<String, AnimationVector1D>(
         { AnimationVector1D(it.toFloat()) }, { it.value.toString() })
@@ -92,6 +92,25 @@
         }
     }
 
+    @OptIn(UiToolingDataApi::class)
+    internal fun ComposeContentTestRule.searchAndTrackAllAnimations(
+        search: AnimationSearch,
+        content: @Composable () -> Unit
+    ) {
+        val slotTableRecord = CompositionDataRecord.create()
+        this.setContent {
+            Inspectable(slotTableRecord) {
+                content()
+            }
+        }
+        this.runOnUiThread {
+            val groups = slotTableRecord.store.map { it.asTree() }
+                .flatMap { tree -> tree.findAll { it.location != null } }
+            search.findAll(groups)
+            search.trackAll()
+        }
+    }
+
     @OptIn(ExperimentalAnimationApi::class)
     @Composable
     fun createTestAnimatedVisibility(): Transition<Boolean> {
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt
new file mode 100644
index 0000000..d1a0b86
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClockTest.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.animateValue
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.AnimationSearch
+import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.Utils.nullableFloatConverter
+import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class InfiniteTransitionClockTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun checkAnimatedPropertiesForAnimateFloat() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                0.2f, 2.1f, infiniteRepeatable(tween(300), RepeatMode.Reverse), label = "Test label"
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnUiThread {
+            // Default state
+            clock.getAnimatedProperties().let {
+                assertEquals(1, it.size)
+                assertEquals(0.2f, it[0].value)
+                assertEquals("Test label", it[0].label)
+            }
+        }
+    }
+
+    @Test
+    fun checkAnimatedPropertiesForAnimateValue() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateValue(
+                30,
+                40,
+                Int.VectorConverter,
+                infiniteRepeatable(tween(300), RepeatMode.Reverse),
+                label = "Test label"
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnUiThread {
+            // Default state
+            clock.getAnimatedProperties().let {
+                assertEquals(1, it.size)
+                assertEquals(30, it[0].value)
+                assertEquals("Test label", it[0].label)
+            }
+        }
+    }
+
+    @Test
+    fun checkAnimatedPropertiesForAnimateColor() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateColor(
+                Color.Red,
+                Color.Green,
+                infiniteRepeatable(tween(300), RepeatMode.Reverse),
+                label = "Test label"
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnUiThread {
+            // Default state
+            clock.getAnimatedProperties().let {
+                assertEquals(1, it.size)
+                assertEquals(Color.Red, it[0].value)
+                assertEquals("Test label", it[0].label)
+            }
+        }
+    }
+
+    @Test
+    fun checkAnimatedPropertiesForNullableAnimateValue() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateValue(
+                30f,
+                null,
+                nullableFloatConverter,
+                infiniteRepeatable(tween(300), RepeatMode.Reverse),
+                label = "Test label"
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnUiThread {
+            // Default state
+            clock.getAnimatedProperties().let {
+                assertEquals(1, it.size)
+                assertEquals(30f, it[0].value)
+                assertEquals("Test label", it[0].label)
+            }
+        }
+    }
+
+    @Test
+    fun checkTransitions() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                0.2f,
+                2.1f,
+                infiniteRepeatable(tween(300), RepeatMode.Reverse),
+                label = "Float label"
+            )
+            infiniteTransition.animateValue(
+                20,
+                30,
+                Int.VectorConverter,
+                infiniteRepeatable(tween(500), RepeatMode.Restart),
+                label = "Int label"
+            )
+            infiniteTransition.animateColor(
+                Color.Red,
+                Color.White,
+                infiniteRepeatable(tween(400), RepeatMode.Reverse),
+                label = "Color label"
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnIdle {
+            val transitions = clock.getTransitions(100)
+            assertEquals(3, transitions.size)
+            transitions[0].let {
+                assertEquals("Float label", it.label)
+                assertEquals(0, it.startTimeMillis)
+                assertEquals(800, it.endTimeMillis)
+                assertTrue(it.specType.contains("InfiniteRepeatableSpec"))
+                assertTrue(it.values.size >= 3)
+                assertTrue(it.values.keys.distinct().size >= 3)
+                assertTrue(it.values.values.distinct().size >= 3)
+            }
+            transitions[1].let {
+                assertEquals("Int label", it.label)
+                assertEquals(0, it.startTimeMillis)
+                assertEquals(800, it.endTimeMillis)
+                assertTrue(it.specType.contains("InfiniteRepeatableSpec"))
+                assertTrue(it.values.size >= 3)
+                assertTrue(it.values.keys.distinct().size >= 3)
+                assertTrue(it.values.values.distinct().size >= 3)
+            }
+            transitions[2].let {
+                assertEquals("Color label", it.label)
+                assertEquals(0, it.startTimeMillis)
+                assertEquals(800, it.endTimeMillis)
+                assertTrue(it.specType.contains("InfiniteRepeatableSpec"))
+                assertTrue(it.values.size >= 3)
+                assertTrue(it.values.keys.distinct().size >= 3)
+                assertTrue(it.values.values.distinct().size >= 3)
+            }
+        }
+    }
+
+    @Test
+    fun checkNullableTransitions() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateValue(
+                30f,
+                null,
+                nullableFloatConverter,
+                infiniteRepeatable(tween(300), RepeatMode.Reverse),
+                label = "Test label"
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnIdle {
+            val transitions = clock.getTransitions(100)
+            assertEquals(1, transitions.size)
+            transitions[0].let {
+                assertEquals("Test label", it.label)
+                assertEquals(0, it.startTimeMillis)
+                assertEquals(600, it.endTimeMillis)
+                assertTrue(it.specType.contains("InfiniteRepeatableSpec"))
+                assertTrue(it.values.size >= 3)
+                assertTrue(it.values.keys.distinct().size >= 3)
+                assertTrue(it.values.values.distinct().size >= 3)
+            }
+        }
+    }
+
+    @Test
+    fun checkDurationOfReverseAnimation() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                0f, 1f,
+                infiniteRepeatable(tween(300), RepeatMode.Reverse),
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnIdle {
+            assertEquals(600, clock.getMaxDurationPerIteration())
+            assertEquals(600, clock.getMaxDuration())
+        }
+    }
+
+    @Test
+    fun checkDurationOfRestartAnimation() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                0f, 1f,
+                infiniteRepeatable(tween(300, 50), RepeatMode.Restart),
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnIdle {
+            assertEquals(350, clock.getMaxDurationPerIteration())
+            assertEquals(350, clock.getMaxDuration())
+        }
+    }
+
+    @Test
+    fun maxDurationIsCorrect() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                0f, 1f,
+                infiniteRepeatable(tween(100), RepeatMode.Restart),
+            )
+            infiniteTransition.animateFloat(
+                0f, 1f,
+                infiniteRepeatable(tween(300), RepeatMode.Restart),
+            )
+            infiniteTransition.animateFloat(
+                0f, 1f,
+                infiniteRepeatable(tween(500), RepeatMode.Restart),
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!)
+        rule.runOnIdle {
+            assertEquals(500, clock.getMaxDurationPerIteration())
+            assertEquals(500, clock.getMaxDuration())
+        }
+    }
+
+    @Test
+    fun maxDurationFromOtherAnimations() {
+        val search = AnimationSearch.InfiniteTransitionSearch { }
+        rule.searchForAnimation(search) {
+            val infiniteTransition = rememberInfiniteTransition()
+            infiniteTransition.animateFloat(
+                0f, 1f,
+                infiniteRepeatable(tween(100), RepeatMode.Restart),
+            )
+        }
+        val clock = InfiniteTransitionClock(search.animations.first().parse()!!) { 1300 }
+        rule.runOnIdle {
+            assertEquals(100, clock.getMaxDurationPerIteration())
+            assertEquals(1300, clock.getMaxDuration())
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
index f05b07e..5b08640 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
@@ -57,6 +57,14 @@
     }
 }
 
+@OptIn(UiToolingDataApi::class)
+private inline fun <reified T> Group.findData(): T? {
+    // Search in self data and children data
+    return (data + children.flatMap { it.data }).firstOrNull { data ->
+        data is T
+    } as? T
+}
+
 /** Contains tree parsers for different animation types. */
 @OptIn(UiToolingDataApi::class)
 internal class AnimationSearch(
@@ -75,18 +83,24 @@
             setOf(AnimateXAsStateSearch { clock().trackAnimateXAsState(it) })
         else emptyList()
 
+    private fun infiniteTransitionSearch() =
+        if (InfiniteTransitionComposeAnimation.apiAvailable)
+            setOf(InfiniteTransitionSearch {
+                clock().trackInfiniteTransition(it)
+            })
+        else emptySet()
+
     /** All supported animations. */
     private fun supportedSearch() = setOf(
         transitionSearch,
         animatedVisibilitySearch,
-    ) + animateXAsStateSearch()
+    ) + animateXAsStateSearch() + infiniteTransitionSearch()
 
     private fun unsupportedSearch() = if (UnsupportedComposeAnimation.apiAvailable) setOf(
         animatedContentSearch,
         AnimateContentSizeSearch { clock().trackAnimateContentSize(it) },
         TargetBasedSearch { clock().trackTargetBasedAnimations(it) },
-        DecaySearch { clock().trackDecayAnimations(it) },
-        InfiniteTransitionSearch { clock().trackInfiniteTransition(it) }
+        DecaySearch { clock().trackDecayAnimations(it) }
     ) else emptyList()
 
     /** All supported animations. */
@@ -172,8 +186,37 @@
     class DecaySearch(trackAnimation: (DecayAnimation<*, *>) -> Unit) :
         RememberSearch<DecayAnimation<*, *>>(DecayAnimation::class, trackAnimation)
 
-    class InfiniteTransitionSearch(trackAnimation: (InfiniteTransition) -> Unit) :
-        RememberSearch<InfiniteTransition>(InfiniteTransition::class, trackAnimation)
+    data class InfiniteTransitionSearchInfo(
+        val infiniteTransition: InfiniteTransition,
+        val toolingState: ToolingState<Long>
+    )
+
+    class InfiniteTransitionSearch(trackAnimation: (InfiniteTransitionSearchInfo) -> Unit) :
+        Search<InfiniteTransitionSearchInfo>(trackAnimation) {
+
+        override fun addAnimations(groupsWithLocation: Collection<Group>) {
+            animations.addAll(findAnimations(groupsWithLocation))
+        }
+
+        private fun findAnimations(groupsWithLocation: Collection<Group>):
+            List<InfiniteTransitionSearchInfo> {
+            val groups = groupsWithLocation.filter { group -> group.name == "run" }
+                .filterIsInstance<CallGroup>()
+            return groups.mapNotNull {
+                val infiniteTransition = it.findData<InfiniteTransition>()
+                val toolingOverride = it.findData<MutableState<State<Long>?>>()
+                if (infiniteTransition != null && toolingOverride != null) {
+                    if (toolingOverride.value == null) {
+                        toolingOverride.value = ToolingState(0L)
+                    }
+                    InfiniteTransitionSearchInfo(
+                        infiniteTransition,
+                        toolingOverride.value as ToolingState<Long>
+                    )
+                } else null
+            }
+        }
+    }
 
     data class AnimateXAsStateSearchInfo<T, V : AnimationVector>(
         val animatable: Animatable<T, V>,
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimation.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimation.kt
new file mode 100644
index 0000000..092f5a1
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/InfiniteTransitionComposeAnimation.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.animation.core.InfiniteTransition
+import androidx.compose.animation.tooling.ComposeAnimation
+import androidx.compose.animation.tooling.ComposeAnimationType
+import org.jetbrains.annotations.TestOnly
+
+/**
+ * [ComposeAnimation] of type [ComposeAnimationType.INFINITE_TRANSITION].
+ */
+internal class InfiniteTransitionComposeAnimation
+private constructor(
+    private val toolingState: ToolingState<Long>,
+    override val animationObject: InfiniteTransition,
+) : ComposeAnimation {
+    override val type = ComposeAnimationType.INFINITE_TRANSITION
+
+    override val states: Set<Any> = setOf(0)
+
+    override val label: String = animationObject.label
+
+    fun setTimeNanos(playTimeNanos: Long) {
+        toolingState.value = playTimeNanos
+    }
+
+    companion object {
+
+        /**
+         * [ComposeAnimationType] from ANIMATABLE to UNSUPPORTED are not available in previous
+         * versions of the library. To avoid creating non-existing enum,
+         * [InfiniteTransitionComposeAnimation] should only be instantiated if
+         * [ComposeAnimationType] API for INFINITE_TRANSITION enum is available.
+         */
+        var apiAvailable =
+            enumValues<ComposeAnimationType>().any { it.name == "INFINITE_TRANSITION" }
+            private set
+
+        internal fun AnimationSearch.InfiniteTransitionSearchInfo.parse():
+            InfiniteTransitionComposeAnimation? {
+            if (!apiAvailable) return null
+            return InfiniteTransitionComposeAnimation(
+                toolingState, infiniteTransition
+            )
+        }
+
+        /** This method is for testing only. */
+        @TestOnly
+        fun testOverrideAvailability(override: Boolean) {
+            apiAvailable = override
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
index 219b884..f3ca7ae 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
@@ -19,16 +19,17 @@
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import androidx.compose.animation.core.DecayAnimation
-import androidx.compose.animation.core.InfiniteTransition
 import androidx.compose.animation.core.TargetBasedAnimation
 import androidx.compose.animation.core.Transition
 import androidx.compose.animation.tooling.ComposeAnimatedProperty
 import androidx.compose.animation.tooling.ComposeAnimation
 import androidx.compose.animation.tooling.TransitionInfo
 import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation.Companion.parse
 import androidx.compose.ui.tooling.animation.clock.AnimateXAsStateClock
 import androidx.compose.ui.tooling.animation.clock.AnimatedVisibilityClock
 import androidx.compose.ui.tooling.animation.clock.ComposeAnimationClock
+import androidx.compose.ui.tooling.animation.clock.InfiniteTransitionClock
 import androidx.compose.ui.tooling.animation.clock.TransitionClock
 import androidx.compose.ui.tooling.animation.clock.millisToNanos
 import androidx.compose.ui.tooling.animation.states.AnimatedVisibilityState
@@ -78,14 +79,24 @@
     internal val animateXAsStateClocks =
         mutableMapOf<AnimateXAsStateComposeAnimation<*, *>, AnimateXAsStateClock<*, *>>()
 
+    /** Map of subscribed [InfiniteTransitionComposeAnimation]s and corresponding [InfiniteTransitionClock]s. */
+    @VisibleForTesting
+    internal val infiniteTransitionClocks =
+        mutableMapOf<InfiniteTransitionComposeAnimation, InfiniteTransitionClock>()
+    private val allClocksExceptInfinite: List<ComposeAnimationClock<*, *>>
+        get() = transitionClocks.values +
+            animatedVisibilityClocks.values +
+            animateXAsStateClocks.values
+
     /** All subscribed animations clocks. */
     private val allClocks: List<ComposeAnimationClock<*, *>>
-        get() = transitionClocks.values +
-            animatedVisibilityClocks.values + animateXAsStateClocks.values
+        get() = allClocksExceptInfinite +
+            infiniteTransitionClocks.values
 
     private fun findClock(animation: ComposeAnimation): ComposeAnimationClock<*, *>? {
         return transitionClocks[animation] ?: animatedVisibilityClocks[animation]
         ?: animateXAsStateClocks[animation]
+        ?: infiniteTransitionClocks[animation]
     }
 
     fun trackTransition(animation: Transition<*>) {
@@ -139,8 +150,22 @@
         trackUnsupported(animation, animation.label ?: "AnimatedContent")
     }
 
-    fun trackInfiniteTransition(animation: InfiniteTransition) {
-        trackUnsupported(animation, "InfiniteTransition")
+    fun trackInfiniteTransition(animation: AnimationSearch.InfiniteTransitionSearchInfo) {
+        trackAnimation(animation.infiniteTransition) {
+            animation.parse()?.let {
+                infiniteTransitionClocks[it] = InfiniteTransitionClock(it) {
+                    // Let InfiniteTransitionClock be aware about max duration of other animations.
+                    val otherClockMaxDuration =
+                        allClocksExceptInfinite.maxOfOrNull { clock -> clock.getMaxDuration() } ?: 0
+                    val infiniteMaxDurationPerIteration =
+                        infiniteTransitionClocks.values.maxOfOrNull { clock ->
+                            clock.getMaxDurationPerIteration()
+                        } ?: 0
+                    maxOf(otherClockMaxDuration, infiniteMaxDurationPerIteration)
+                }
+                notifySubscribe(it)
+            }
+        }
     }
 
     @VisibleForTesting
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClock.kt
new file mode 100644
index 0000000..2cc56af
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/InfiniteTransitionClock.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.InfiniteRepeatableSpec
+import androidx.compose.animation.core.InfiniteTransition
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.tooling.ComposeAnimatedProperty
+import androidx.compose.animation.tooling.TransitionInfo
+import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation
+import androidx.compose.ui.tooling.animation.states.TargetState
+import kotlin.math.max
+
+/**
+ * [ComposeAnimationClock] for [InfiniteTransition] animations.
+ *
+ *  @sample androidx.compose.animation.samples.InfiniteTransitionSample
+ */
+internal class InfiniteTransitionClock(
+    override val animation: InfiniteTransitionComposeAnimation,
+    private val maxDuration: () -> Long = { 0 }
+) :
+    ComposeAnimationClock<InfiniteTransitionComposeAnimation, TargetState<Any>> {
+
+    /** [rememberInfiniteTransition] doesn't have a state. */
+    override var state: TargetState<Any> = TargetState(0, 0)
+
+    override fun setStateParameters(par1: Any, par2: Any?) {}
+
+    override fun getAnimatedProperties(): List<ComposeAnimatedProperty> {
+        return animation.animationObject.animations.mapNotNull {
+            val value = it.value
+            value ?: return@mapNotNull null
+            ComposeAnimatedProperty(it.label, value)
+        }.filter { !IGNORE_TRANSITIONS.contains(it.label) }
+    }
+
+    /** Max duration per iteration of the animation. */
+    override fun getMaxDurationPerIteration(): Long {
+        return nanosToMillis(animation.animationObject.animations.maxOfOrNull {
+            it.getIterationDuration()
+        } ?: 0)
+    }
+
+    /** Max duration of the animation. */
+    override fun getMaxDuration(): Long {
+        return max(getMaxDurationPerIteration(), maxDuration())
+    }
+
+    override fun getTransitions(stepMillis: Long): List<TransitionInfo> {
+        val transition = animation.animationObject
+        return transition.animations.map {
+            it.createTransitionInfo(stepMillis, getMaxDuration())
+        }.filter { !IGNORE_TRANSITIONS.contains(it.label) }.toList()
+    }
+
+    override fun setClockTime(animationTimeNanos: Long) {
+        animation.setTimeNanos(animationTimeNanos)
+    }
+
+    private fun <T, V : AnimationVector> InfiniteTransition.TransitionAnimationState<T, V>
+        .getIterationDuration(): Long {
+        val repeatableSpec = animationSpec as InfiniteRepeatableSpec<T>
+        // If animation has Reverse mode, include two iterations, otherwise just one.
+        val repeats = if (repeatableSpec.repeatMode == RepeatMode.Reverse) 2 else 1
+        val animation = repeatableSpec.animation.vectorize(typeConverter)
+        return millisToNanos(animation.delayMillis.toLong() + animation.durationMillis * repeats)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
index abd7673..13dbdcd 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
@@ -20,6 +20,7 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.AnimationVector
 import androidx.compose.animation.core.InfiniteRepeatableSpec
+import androidx.compose.animation.core.InfiniteTransition
 import androidx.compose.animation.core.KeyframesSpec
 import androidx.compose.animation.core.RepeatableSpec
 import androidx.compose.animation.core.SnapSpec
@@ -29,6 +30,9 @@
 import androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec
 import androidx.compose.animation.tooling.TransitionInfo
 
+/** Animations can contain internal only transitions which should be ignored by tooling. */
+internal val IGNORE_TRANSITIONS = listOf("TransformOriginInterruptionHandling")
+
 /**
  * Converts the given time in nanoseconds to milliseconds, rounding up when needed.
  */
@@ -111,4 +115,32 @@
         label, animationSpec.javaClass.name,
         startTimeMs, endTimeMs, values
     )
+}
+
+/**
+ * Creates [TransitionInfo] for [InfiniteTransition.TransitionAnimationState].
+ */
+internal fun <T, V : AnimationVector>
+    InfiniteTransition.TransitionAnimationState<T, V>.createTransitionInfo(
+    stepMs: Long = 1,
+    endTimeMs: Long
+): TransitionInfo {
+    val startTimeMs: Long = 0
+    val values: Map<Long, T> by lazy {
+        val values: MutableMap<Long, T> = mutableMapOf()
+        // Always add start and end points.
+        values[startTimeMs] = this.animation.getValueFromNanos(
+            millisToNanos(startTimeMs)
+        )
+        values[endTimeMs] = this.animation.getValueFromNanos(millisToNanos(endTimeMs))
+
+        for (millis in startTimeMs..endTimeMs step stepMs) {
+            values[millis] = this.animation.getValueFromNanos(millisToNanos(millis))
+        }
+        values
+    }
+    return TransitionInfo(
+        label, animationSpec.javaClass.name,
+        startTimeMs, endTimeMs, values
+    )
 }
\ No newline at end of file