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