Merge "Return all descendant animations in getAnimatedProperties" into androidx-main
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 b0c394f..2b06bd8 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
@@ -20,10 +20,13 @@
 import androidx.compose.animation.EnterExitState
 import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.ExperimentalTransitionApi
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.core.LinearEasing
 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.tween
 import androidx.compose.animation.core.updateTransition
 import androidx.compose.animation.fadeIn
@@ -41,6 +44,7 @@
 import androidx.test.filters.MediumTest
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
@@ -95,7 +99,7 @@
         assertEquals(72f, rotation.value as Float, eps)
 
         animatedProperties = testClock.getAnimatedProperties(offsetAnimation!!)
-        val offset = animatedProperties.single()
+        val offset = animatedProperties.single { it.label == "myOffset" }
         // We're animating from O1 (0) to O2 (100). There is a transition of 800ms defined for
         // the offset, and we set the clock to 25% of this time.
         assertEquals(25f, offset.value as Float, eps)
@@ -122,6 +126,24 @@
     }
 
     @Test
+    fun getAnimatedPropertiesReturnsAllDescendantAnimations() {
+        var transitionAnimation: ComposeAnimation? = null
+
+        composeRule.setContent {
+            transitionAnimation = setUpOffsetScenario()
+        }
+        composeRule.waitForIdle()
+
+        val animatedProperties = testClock.getAnimatedProperties(transitionAnimation!!)
+        // getAnimatedProperties should return all the transition animations as well as the
+        // animations of all descendant transitions
+        assertNotNull(animatedProperties.single { it.label == "myOffset" })
+        assertNotNull(animatedProperties.single { it.label == "child1 scale" })
+        assertNotNull(animatedProperties.single { it.label == "child2 color" })
+        assertNotNull(animatedProperties.single { it.label == "grandchild" })
+    }
+
+    @Test
     fun getAnimatedPropertiesReturnsChildAnimations() {
         var animatedVisibility: ComposeAnimation? = null
 
@@ -357,6 +379,9 @@
     }
 
     // Sets up a transition animation scenario, going from from Offset.O1 to Offset.O2.
+    // The main transition in this scenario also has 2 child animations. One of them has a child
+    // animation of its own.
+    @OptIn(ExperimentalTransitionApi::class)
     @Suppress("UNCHECKED_CAST")
     @Composable
     private fun setUpOffsetScenario(): ComposeAnimation {
@@ -373,6 +398,21 @@
             }
         }
 
+        val child1 = transition.createChildTransition { it == Offset.O1 }
+        child1.animateFloat(label = "child1 scale") { pressed ->
+            if (pressed) 1f else 3f
+        }
+
+        child1.createChildTransition { it }
+            .animateDp(label = "grandchild") { parentState ->
+                if (parentState) 1.dp else 3.dp
+            }
+
+        transition.createChildTransition { it }
+            .animateColor(label = "child2 color") { state ->
+                if (state == Offset.O1) Color.Red else Color.Blue
+            }
+
         testClock.trackTransition(transition as Transition<Any>)
         val animation = testClock.trackedTransitions.single { it.states.contains(Offset.O1) }
         testClock.updateFromAndToStates(animation, Offset.O1, Offset.O2)
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 950f573..03aaf36 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
@@ -231,16 +231,13 @@
         if (trackedTransitions.contains(animation)) {
             val transition = (animation as TransitionComposeAnimation).animationObject
             // In case the transition have child transitions, make sure to return their
-            // animations as well.
-            // TODO(b/187962923): support indirect descendants, e.g. grandchildren animations.
-            val animations =
-                transition.animations + transition.transitions.flatMap { it.animations }
-            return animations.mapNotNull {
+            // descendant animations as well.
+            return transition.allAnimations().mapNotNull {
                 ComposeAnimatedProperty(it.label, it.value ?: return@mapNotNull null)
             }
         } else if (trackedAnimatedVisibility.contains(animation)) {
             (animation as AnimatedVisibilityComposeAnimation).childTransition?.let { child ->
-                return child.animations.mapNotNull {
+                return child.allAnimations().mapNotNull {
                     ComposeAnimatedProperty(it.label, it.value ?: return@mapNotNull null)
                 }
             }
@@ -293,4 +290,13 @@
 
     private fun AnimatedVisibilityState.toCurrentTargetPair() =
         if (this == AnimatedVisibilityState.Enter) false to true else true to false
+
+    /**
+     * Return all the animations of a [Transition], as well as all the animations of its every
+     * descendant [Transition]s.
+     */
+    private fun Transition<*>.allAnimations(): List<Transition<*>.TransitionAnimationState<*, *>> {
+        val descendantAnimations = transitions.flatMap { it.allAnimations() }
+        return animations + descendantAnimations
+    }
 }