summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt351
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt57
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt77
3 files changed, 172 insertions, 313 deletions
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
index b5723c3053e1..c6ef8cff1a66 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
@@ -27,11 +27,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.AutoTransitionTestAssertionScope
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.Default4FrameLinearTransition
import com.android.compose.animation.scene.Edge
@@ -58,6 +61,37 @@ class NestedSharedElementTest {
val NestedNestedSceneB = SceneKey("NestedNestedSceneB")
}
+ private val elementVariant1 = SharedElement(0.dp, 0.dp, 100.dp, 100.dp, Color.Red)
+ private val elementVariant2 = SharedElement(40.dp, 80.dp, 60.dp, 20.dp, Color.Blue)
+ private val elementVariant3 = SharedElement(80.dp, 40.dp, 140.dp, 180.dp, Color.Yellow)
+ private val elementVariant4 = SharedElement(120.dp, 240.dp, 20.dp, 140.dp, Color.Green)
+
+ private class SharedElement(
+ val x: Dp,
+ val y: Dp,
+ val width: Dp,
+ val height: Dp,
+ val color: Color = Color.Black,
+ val alpha: Float = 0.8f,
+ )
+
+ @Composable
+ private fun ContentScope.SharedElement(element: SharedElement) {
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier.offset(element.x, element.y)
+ .element(TestElements.Foo)
+ .size(element.width, element.height)
+ .background(element.color)
+ .alpha(element.alpha)
+ )
+ }
+ }
+
+ private val contentWithSharedElement: @Composable ContentScope.() -> Unit = {
+ SharedElement(elementVariant1)
+ }
+
private val nestedState: MutableSceneTransitionLayoutState =
rule.runOnUiThread {
MutableSceneTransitionLayoutState(
@@ -88,29 +122,8 @@ class NestedSharedElementTest {
private val nestedStlWithSharedElement: @Composable ContentScope.() -> Unit = {
NestedSceneTransitionLayout(nestedState, modifier = Modifier) {
- scene(Scenes.NestedSceneB) {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier.offset(50.dp, 10.dp)
- .element(TestElements.Foo)
- .size(60.dp, 40.dp)
- .background(Color.Blue)
- .alpha(0.8f)
- )
- }
- }
- scene(Scenes.NestedSceneA) {
- Box(Modifier.fillMaxSize()) {
- // Foo is at (10, 50) with a size of (20, 80).
- Box(
- Modifier.offset(10.dp, 50.dp)
- .element(TestElements.Foo)
- .size(20.dp, 80.dp)
- .background(Color.Red)
- .alpha(0.8f)
- )
- }
- }
+ scene(Scenes.NestedSceneA) { SharedElement(elementVariant2) }
+ scene(Scenes.NestedSceneB) { SharedElement(elementVariant3) }
}
}
@@ -118,54 +131,11 @@ class NestedSharedElementTest {
NestedSceneTransitionLayout(nestedState, modifier = Modifier) {
scene(Scenes.NestedSceneA) {
NestedSceneTransitionLayout(state = nestedNestedState, modifier = Modifier) {
- scene(Scenes.NestedNestedSceneA) {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier.offset(130.dp, 90.dp)
- .element(TestElements.Foo)
- .size(100.dp, 80.dp)
- .background(Color.DarkGray)
- .alpha(0.8f)
- )
- }
- }
- scene(Scenes.NestedNestedSceneB) {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier.offset(50.dp, 10.dp)
- .element(TestElements.Foo)
- .size(60.dp, 40.dp)
- .background(Color.Blue)
- .alpha(0.8f)
- )
- }
- }
+ scene(Scenes.NestedNestedSceneA) { SharedElement(elementVariant4) }
+ scene(Scenes.NestedNestedSceneB) { SharedElement(elementVariant3) }
}
}
- scene(Scenes.NestedSceneB) {
- Box(Modifier.fillMaxSize()) {
- // Foo is at (10, 50) with a size of (20, 80).
- Box(
- Modifier.offset(10.dp, 50.dp)
- .element(TestElements.Foo)
- .size(20.dp, 80.dp)
- .background(Color.Red)
- .alpha(0.8f)
- )
- }
- }
- }
- }
-
- private val contentWithSharedElement: @Composable ContentScope.() -> Unit = {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier.offset(50.dp, 70.dp)
- .element(TestElements.Foo)
- .size(10.dp, 40.dp)
- .background(Color.Magenta)
- .alpha(0.8f)
- )
+ scene(Scenes.NestedSceneB) { SharedElement(elementVariant2) }
}
}
@@ -175,44 +145,14 @@ class NestedSharedElementTest {
fromSceneContent = nestedStlWithSharedElement,
toSceneContent = contentWithSharedElement,
) {
- before {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp)
- }
- at(0) {
- // Shared elements are by default placed and drawn only in the scene with highest
- // zIndex.
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- .assertSizeIsEqualTo(20.dp, 80.dp)
- }
- at(16) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(20.dp, 55.dp)
- .assertSizeIsEqualTo(17.5.dp, 70.dp)
- }
- at(32) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ atAllFrames(4) {
onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(30.dp, 60.dp)
- .assertSizeIsEqualTo(15.dp, 60.dp)
- }
- at(48) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(40.dp, 65.dp)
- .assertSizeIsEqualTo(12.5.dp, 50.dp)
- }
- after {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(10.dp, 40.dp)
+ .assertBetweenElementVariants(elementVariant2, elementVariant1, this)
}
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
}
}
@@ -222,44 +162,14 @@ class NestedSharedElementTest {
fromSceneContent = contentWithSharedElement,
toSceneContent = nestedStlWithSharedElement,
) {
- before {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(10.dp, 40.dp)
- }
- at(0) {
- // Shared elements placed in NestedSTLs are by default drawn only in the STL with
- // the lowest nestingDepth and the scene in which the element is placed directly.
- onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(50.dp, 70.dp)
- .assertSizeIsEqualTo(10.dp, 40.dp)
- }
- at(16) {
- onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(40.dp, 65.dp)
- .assertSizeIsEqualTo(12.5.dp, 50.dp)
- }
- at(32) {
- onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(30.dp, 60.dp)
- .assertSizeIsEqualTo(15.dp, 60.dp)
- }
- at(48) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ atAllFrames(4) {
onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(20.dp, 55.dp)
- .assertSizeIsEqualTo(17.5.dp, 70.dp)
- }
- after {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp)
+ .assertBetweenElementVariants(elementVariant1, elementVariant2, this)
}
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
}
}
@@ -269,44 +179,14 @@ class NestedSharedElementTest {
fromSceneContent = contentWithSharedElement,
toSceneContent = nestedNestedStlWithSharedElement,
) {
- before {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(10.dp, 40.dp)
- }
- at(0) {
- // Shared elements placed in NestedSTLs are by default drawn only in the STL with
- // the lowest nestingDepth and the scene in which the element is placed directly.
- onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(50.dp, 70.dp)
- .assertSizeIsEqualTo(10.dp, 40.dp)
- }
- at(16) {
- onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(70.dp, 75.dp)
- .assertSizeIsEqualTo(32.5.dp, 50.dp)
- }
- at(32) {
- onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(90.dp, 80.dp)
- .assertSizeIsEqualTo(55.dp, 60.dp)
- }
- at(48) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ atAllFrames(4) {
onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
onElement(TestElements.Foo, TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(110.dp, 85.dp)
- .assertSizeIsEqualTo(77.5.dp, 70.dp)
- }
- after {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(130.dp, 90.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 80.dp)
+ .assertBetweenElementVariants(elementVariant1, elementVariant4, this)
}
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant4) }
}
}
@@ -317,49 +197,15 @@ class NestedSharedElementTest {
toSceneContent = { Box(modifier = Modifier.fillMaxSize()) },
changeState = { nestedState.setTargetScene(Scenes.NestedSceneB, this) },
) {
- before {
- onElement(TestElements.Foo)
- .assertPositionInRootIsEqualTo(130.dp, 90.dp)
- .assertSizeIsEqualTo(100.dp, 80.dp)
- }
- at(0) {
- // Shared elements placed in NestedSTLs are by default drawn only in the STL with
- // the lowest nestingDepth and the scene in which the element is placed directly.
- onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
- onElement(TestElements.Foo, Scenes.NestedNestedSceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, Scenes.NestedSceneB)
- .assertPositionInRootIsEqualTo(130.dp, 90.dp)
- .assertSizeIsEqualTo(100.dp, 80.dp)
- }
- at(16) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant4) }
+ atAllFrames(4) {
onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
onElement(TestElements.Foo, Scenes.NestedNestedSceneA).assertIsNotDisplayed()
onElement(TestElements.Foo, Scenes.NestedSceneB)
- .assertPositionInRootIsEqualTo(100.dp, 80.dp)
- .assertSizeIsEqualTo(80.dp, 80.dp)
- }
- at(32) {
- onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
- onElement(TestElements.Foo, Scenes.NestedNestedSceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, Scenes.NestedSceneB)
- .assertPositionInRootIsEqualTo(70.dp, 70.dp)
- .assertSizeIsEqualTo(60.dp, 80.dp)
- }
- at(48) {
- onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
- onElement(TestElements.Foo, Scenes.NestedNestedSceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, Scenes.NestedSceneB)
- .assertPositionInRootIsEqualTo(40.dp, 60.dp)
- .assertSizeIsEqualTo(40.dp, 80.dp)
- }
- after {
- onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp)
- onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp)
+ .assertBetweenElementVariants(elementVariant4, elementVariant2, this)
}
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
}
}
@@ -380,75 +226,58 @@ class NestedSharedElementTest {
// We can't reference the element inside the NestedSTL as of today
},
) {
- before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp) }
- at(0) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(50.dp, 70.dp)
- .assertSizeIsEqualTo(10.dp, 40.dp)
- }
- at(16) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(37.5.dp, 70.dp)
- }
- at(32) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(25.dp, 70.dp)
- }
- at(48) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ atAllFrames(4) {
onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(12.5.dp, 70.dp)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant1.x, 0.dp),
+ elementVariant1.y,
+ )
+ .assertSizeIsEqualTo(elementVariant1.width, elementVariant1.height)
}
- after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
}
}
@Test
fun nestedSharedElementTransition_transitionInsideNestedStl() {
rule.testTransition(
- layoutModifier = Modifier.fillMaxSize().background(Color.Gray),
+ layoutModifier = Modifier.fillMaxSize(),
fromSceneContent = nestedStlWithSharedElement,
toSceneContent = contentWithSharedElement,
changeState = { nestedState.setTargetScene(Scenes.NestedSceneB, animationScope = this) },
) {
- before {
- onElement(TestElements.Foo)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- .assertSizeIsEqualTo(20.dp, 80.dp)
- }
- at(0) {
- onElement(TestElements.Foo, Scenes.NestedSceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, scene = Scenes.NestedSceneA)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- .assertSizeIsEqualTo(20.dp, 80.dp)
- }
- at(16) {
- onElement(TestElements.Foo, Scenes.NestedSceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, scene = Scenes.NestedSceneA)
- .assertPositionInRootIsEqualTo(20.dp, 40.dp)
- .assertSizeIsEqualTo(30.dp, 70.dp)
- }
- at(32) {
- onElement(TestElements.Foo, Scenes.NestedSceneB).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, scene = Scenes.NestedSceneA)
- .assertPositionInRootIsEqualTo(30.dp, 30.dp)
- .assertSizeIsEqualTo(40.dp, 60.dp)
- }
- at(48) {
- onElement(TestElements.Foo, Scenes.NestedSceneB).assertIsNotDisplayed()
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
- onElement(TestElements.Foo, scene = Scenes.NestedSceneA)
- .assertPositionInRootIsEqualTo(40.dp, 20.dp)
- .assertSizeIsEqualTo(50.dp, 50.dp)
+ onElement(TestElements.Foo, scene = Scenes.NestedSceneB)
+ .assertBetweenElementVariants(elementVariant2, elementVariant3, this)
}
after {
onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
- onElement(TestElements.Foo)
- .assertPositionInRootIsEqualTo(50.dp, 10.dp)
- .assertSizeIsEqualTo(60.dp, 40.dp)
+ onElement(TestElements.Foo).assertElementVariant(elementVariant3)
}
}
}
+
+ private fun SemanticsNodeInteraction.assertElementVariant(variant: SharedElement) {
+ assertPositionInRootIsEqualTo(variant.x, variant.y)
+ assertSizeIsEqualTo(variant.width, variant.height)
+ }
+
+ private fun SemanticsNodeInteraction.assertBetweenElementVariants(
+ from: SharedElement,
+ to: SharedElement,
+ assertScope: AutoTransitionTestAssertionScope,
+ ) {
+ assertPositionInRootIsEqualTo(
+ assertScope.interpolate(from.x, to.x),
+ assertScope.interpolate(from.y, to.y),
+ )
+ assertSizeIsEqualTo(
+ assertScope.interpolate(from.width, to.width),
+ assertScope.interpolate(from.height, to.height),
+ )
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index 2e3a934c2701..47c10f5ab3a3 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -62,35 +62,14 @@ class SharedElementTest {
onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp)
onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp)
}
- at(0) {
- // Shared elements are by default placed and drawn only in the scene with highest
- // zIndex.
+ atAllFrames(4) {
onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- .assertSizeIsEqualTo(20.dp, 80.dp)
- }
- at(16) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(20.dp, 55.dp)
- .assertSizeIsEqualTo(17.5.dp, 70.dp)
- }
- at(32) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(30.dp, 60.dp)
- .assertSizeIsEqualTo(15.dp, 60.dp)
- }
- at(48) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(40.dp, 65.dp)
- .assertSizeIsEqualTo(12.5.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(
+ interpolate(10.dp, 50.dp),
+ interpolate(50.dp, 70.dp),
+ )
+ .assertSizeIsEqualTo(interpolate(20.dp, 10.dp), interpolate(80.dp, 40.dp))
}
after {
onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp)
@@ -132,29 +111,11 @@ class SharedElementTest {
},
) {
before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
- at(0) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 100.dp)
- }
- at(16) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(7.5.dp, 50.dp)
- onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 90.dp)
- }
- at(32) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(5.dp, 50.dp)
- onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 80.dp)
- }
- at(48) {
+ atAllFrames(4) {
onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(2.5.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(interpolate(10.dp, 0.dp), 50.dp)
onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 70.dp)
+ .assertPositionInRootIsEqualTo(50.dp, interpolate(100.dp, 60.dp))
}
after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 60.dp) }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index 171a9601913c..124b61e45ed6 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -29,6 +29,9 @@ import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.lerp
+import androidx.compose.ui.util.lerp
import kotlinx.coroutines.CoroutineScope
import platform.test.motion.MotionTestRule
import platform.test.motion.RecordedMotion
@@ -64,6 +67,16 @@ interface TransitionTestBuilder {
fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit)
/**
+ * Run the same assertion for all frames of a transition.
+ *
+ * @param totalFrames needs to be the exact number of frames of the transition that is run,
+ * otherwise the passed progress will be incorrect. That is the duration in ms divided by 16.
+ * @param builder is passed a progress Float which can be used to calculate values for the
+ * specific frame. Or use [AutoTransitionTestAssertionScope.interpolate].
+ */
+ fun atAllFrames(totalFrames: Int, builder: AutoTransitionTestAssertionScope.(Float) -> Unit)
+
+ /**
* Assert on the state of the layout after the transition finished.
*
* This should be called maximum once, after [before] or [at] is called.
@@ -84,6 +97,12 @@ interface TransitionTestAssertionScope {
fun onElement(element: ElementKey, scene: SceneKey? = null): SemanticsNodeInteraction
}
+interface AutoTransitionTestAssertionScope : TransitionTestAssertionScope {
+
+ /** Linear interpolate [from] and [to] with the current progress of the transition. */
+ fun <T> interpolate(from: T, to: T): T
+}
+
val Default4FrameLinearTransition: TransitionBuilder.() -> Unit = {
spec = tween(16 * 4, easing = LinearEasing)
}
@@ -302,13 +321,30 @@ fun ComposeContentTestRule.testTransition(
) {
val test = transitionTest(builder)
val assertionScope =
- object : TransitionTestAssertionScope {
+ object : AutoTransitionTestAssertionScope {
+ var progress = 0f
+
override fun onElement(
element: ElementKey,
scene: SceneKey?,
): SemanticsNodeInteraction {
return onNode(isElement(element, scene))
}
+
+ override fun <T> interpolate(from: T, to: T): T {
+ @Suppress("UNCHECKED_CAST")
+ return when {
+ from is Float && to is Float -> lerp(from, to, progress)
+ from is Int && to is Int -> lerp(from, to, progress)
+ from is Long && to is Long -> lerp(from, to, progress)
+ from is Dp && to is Dp -> lerp(from, to, progress)
+ else ->
+ throw UnsupportedOperationException(
+ "Interpolation not supported for this type"
+ )
+ }
+ as T
+ }
}
lateinit var coroutineScope: CoroutineScope
@@ -330,14 +366,28 @@ fun ComposeContentTestRule.testTransition(
mainClock.advanceTimeByFrame()
waitForIdle()
+ var currentTime = 0L
// Test the assertions at specific points in time.
test.timestamps.forEach { tsAssertion ->
if (tsAssertion.timestampDelta > 0L) {
mainClock.advanceTimeBy(tsAssertion.timestampDelta)
waitForIdle()
+ currentTime += tsAssertion.timestampDelta.toInt()
}
- tsAssertion.assertion(assertionScope)
+ assertionScope.progress = tsAssertion.progress
+ try {
+ tsAssertion.assertion(assertionScope, tsAssertion.progress)
+ } catch (assertionError: AssertionError) {
+ if (assertionScope.progress > 0) {
+ throw AssertionError(
+ "Transition assertion failed at ${currentTime}ms " +
+ "at progress: ${assertionScope.progress}f",
+ assertionError,
+ )
+ }
+ throw assertionError
+ }
}
// Go to the end state and test it.
@@ -380,7 +430,25 @@ private fun transitionTest(builder: TransitionTestBuilder.() -> Unit): Transitio
val delta = timestamp - currentTimestamp
currentTimestamp = timestamp
- timestamps.add(TimestampAssertion(delta, builder))
+ timestamps.add(TimestampAssertion(delta, { builder() }, 0f))
+ }
+
+ override fun atAllFrames(
+ totalFrames: Int,
+ builder: AutoTransitionTestAssertionScope.(Float) -> Unit,
+ ) {
+ check(after == null) { "atFrames(...) {} must be called before after {}" }
+ check(currentTimestamp == 0L) {
+ "atFrames(...) can't be called multiple times or after at(...)"
+ }
+
+ for (frame in 0 until totalFrames) {
+ val timestamp = frame * 16L
+ val delta = timestamp - currentTimestamp
+ val progress = frame.toFloat() / totalFrames
+ currentTimestamp = timestamp
+ timestamps.add(TimestampAssertion(delta, builder, progress))
+ }
}
override fun after(builder: TransitionTestAssertionScope.() -> Unit) {
@@ -405,5 +473,6 @@ private class TransitionTest(
private class TimestampAssertion(
val timestampDelta: Long,
- val assertion: TransitionTestAssertionScope.() -> Unit,
+ val assertion: AutoTransitionTestAssertionScope.(Float) -> Unit,
+ val progress: Float,
)