summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt8
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt144
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt13
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt188
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt34
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt17
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt212
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt4
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt111
16 files changed, 650 insertions, 101 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
index 2bc8c87978a8..b16673702b49 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
@@ -26,15 +26,15 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
internal fun CoroutineScope.animateContent(
+ layoutState: MutableSceneTransitionLayoutStateImpl,
transition: TransitionState.Transition,
oneOffAnimation: OneOffAnimation,
targetProgress: Float,
- startTransition: () -> Unit,
- finishTransition: () -> Unit,
+ chain: Boolean = true,
) {
// Start the transition. This will compute the TransformationSpec associated to [transition],
// which we need to initialize the Animatable that will actually animate it.
- startTransition()
+ layoutState.startTransition(transition, chain)
// The transition now contains the transformation spec that we should use to instantiate the
// Animatable.
@@ -59,7 +59,7 @@ internal fun CoroutineScope.animateContent(
try {
animatable.animateTo(targetProgress, animationSpec, initialVelocity)
} finally {
- finishTransition()
+ layoutState.finishTransition(transition)
}
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt
new file mode 100644
index 000000000000..e020f14a9a02
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene
+
+import com.android.compose.animation.scene.content.state.TransitionState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+
+/** Trigger a one-off transition to show or hide an overlay. */
+internal fun CoroutineScope.showOrHideOverlay(
+ layoutState: MutableSceneTransitionLayoutStateImpl,
+ overlay: OverlayKey,
+ fromOrToScene: SceneKey,
+ isShowing: Boolean,
+ transitionKey: TransitionKey?,
+ replacedTransition: TransitionState.Transition.ShowOrHideOverlay?,
+ reversed: Boolean,
+): TransitionState.Transition.ShowOrHideOverlay {
+ val targetProgress = if (reversed) 0f else 1f
+ val (fromContent, toContent) =
+ if (isShowing xor reversed) {
+ fromOrToScene to overlay
+ } else {
+ overlay to fromOrToScene
+ }
+
+ val oneOffAnimation = OneOffAnimation()
+ val transition =
+ OneOffShowOrHideOverlayTransition(
+ overlay = overlay,
+ fromOrToScene = fromOrToScene,
+ fromContent = fromContent,
+ toContent = toContent,
+ isEffectivelyShown = isShowing,
+ key = transitionKey,
+ replacedTransition = replacedTransition,
+ oneOffAnimation = oneOffAnimation,
+ )
+
+ animateContent(
+ layoutState = layoutState,
+ transition = transition,
+ oneOffAnimation = oneOffAnimation,
+ targetProgress = targetProgress,
+ )
+
+ return transition
+}
+
+/** Trigger a one-off transition to replace an overlay by another one. */
+internal fun CoroutineScope.replaceOverlay(
+ layoutState: MutableSceneTransitionLayoutStateImpl,
+ fromOverlay: OverlayKey,
+ toOverlay: OverlayKey,
+ transitionKey: TransitionKey?,
+ replacedTransition: TransitionState.Transition.ReplaceOverlay?,
+ reversed: Boolean,
+): TransitionState.Transition.ReplaceOverlay {
+ val targetProgress = if (reversed) 0f else 1f
+ val effectivelyShownOverlay = if (reversed) fromOverlay else toOverlay
+
+ val oneOffAnimation = OneOffAnimation()
+ val transition =
+ OneOffOverlayReplacingTransition(
+ fromOverlay = fromOverlay,
+ toOverlay = toOverlay,
+ effectivelyShownOverlay = effectivelyShownOverlay,
+ key = transitionKey,
+ replacedTransition = replacedTransition,
+ oneOffAnimation = oneOffAnimation,
+ )
+
+ animateContent(
+ layoutState = layoutState,
+ transition = transition,
+ oneOffAnimation = oneOffAnimation,
+ targetProgress = targetProgress,
+ )
+
+ return transition
+}
+
+private class OneOffShowOrHideOverlayTransition(
+ overlay: OverlayKey,
+ fromOrToScene: SceneKey,
+ fromContent: ContentKey,
+ toContent: ContentKey,
+ override val isEffectivelyShown: Boolean,
+ override val key: TransitionKey?,
+ replacedTransition: TransitionState.Transition?,
+ private val oneOffAnimation: OneOffAnimation,
+) :
+ TransitionState.Transition.ShowOrHideOverlay(
+ overlay,
+ fromOrToScene,
+ fromContent,
+ toContent,
+ replacedTransition,
+ ) {
+ override val progress: Float
+ get() = oneOffAnimation.progress
+
+ override val progressVelocity: Float
+ get() = oneOffAnimation.progressVelocity
+
+ override val isInitiatedByUserInput: Boolean = false
+ override val isUserInputOngoing: Boolean = false
+
+ override fun finish(): Job = oneOffAnimation.finish()
+}
+
+private class OneOffOverlayReplacingTransition(
+ fromOverlay: OverlayKey,
+ toOverlay: OverlayKey,
+ override val effectivelyShownOverlay: OverlayKey,
+ override val key: TransitionKey?,
+ replacedTransition: TransitionState.Transition?,
+ private val oneOffAnimation: OneOffAnimation,
+) : TransitionState.Transition.ReplaceOverlay(fromOverlay, toOverlay, replacedTransition) {
+ override val progress: Float
+ get() = oneOffAnimation.progress
+
+ override val progressVelocity: Float
+ get() = oneOffAnimation.progressVelocity
+
+ override val isInitiatedByUserInput: Boolean = false
+ override val isUserInputOngoing: Boolean = false
+
+ override fun finish(): Job = oneOffAnimation.finish()
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 7eef5d63d7fd..4aa50b586c1b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -412,7 +412,7 @@ private class AnimatedStateImpl<T, Delta>(
if (canOverflow) transition.progress
else transition.progress.fastCoerceIn(0f, 1f)
}
- overscrollSpec.scene == transition.toContent -> 1f
+ overscrollSpec.content == transition.toContent -> 1f
else -> 0f
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 8aa069067347..abe079a4ab64 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -166,11 +166,11 @@ private fun CoroutineScope.animateToScene(
}
animateContent(
+ layoutState = layoutState,
transition = transition,
oneOffAnimation = oneOffAnimation,
targetProgress = targetProgress,
- startTransition = { layoutState.startTransition(transition, chain) },
- finishTransition = { layoutState.finishTransition(transition) },
+ chain = chain,
)
return transition
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 6ea0285742af..9b1740dc700a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -310,9 +310,10 @@ internal class ElementNode(
val transition = elementState as? TransitionState.Transition
- // If this element is not supposed to be laid out now because the other content of its
- // transition is overscrolling, then lay out the element normally and don't place it.
- val overscrollScene = transition?.currentOverscrollSpec?.scene
+ // If this element is not supposed to be laid out now, either because it is not part of any
+ // ongoing transition or the other content of its transition is overscrolling, then lay out
+ // the element normally and don't place it.
+ val overscrollScene = transition?.currentOverscrollSpec?.content
val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key
if (isOtherSceneOverscrolling) {
return doNotPlace(measurable, constraints)
@@ -845,7 +846,7 @@ internal fun shouldPlaceOrComposeSharedElement(
transition: TransitionState.Transition,
): Boolean {
// If we are overscrolling, only place/compose the element in the overscrolling scene.
- val overscrollScene = transition.currentOverscrollSpec?.scene
+ val overscrollScene = transition.currentOverscrollSpec?.content
if (overscrollScene != null) {
return content == overscrollScene
}
@@ -1184,7 +1185,7 @@ private inline fun <T> computeValue(
val currentContent = currentContentState.content
if (transition is TransitionState.HasOverscrollProperties) {
val overscroll = transition.currentOverscrollSpec
- if (overscroll?.scene == currentContent) {
+ if (overscroll?.content == currentContent) {
val elementSpec =
overscroll.transformationSpec.transformations(element.key, currentContent)
val propertySpec = transformation(elementSpec) ?: return currentValue()
@@ -1210,7 +1211,7 @@ private inline fun <T> computeValue(
// TODO(b/290184746): Make sure that we don't overflow transformations associated to a
// range.
val directionSign = if (transition.isUpOrLeft) -1 else 1
- val isToContent = overscroll.scene == transition.toContent
+ val isToContent = overscroll.content == transition.toContent
val linearProgress = transition.progress.let { if (isToContent) it - 1f else it }
val progressConverter =
overscroll.progressConverter
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 21f11e4a4f9e..5f5141e1f153 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -432,7 +432,7 @@ private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) :
val progress =
when {
overscrollSpec == null -> transition.progress
- overscrollSpec.scene == transition.toScene -> 1f
+ overscrollSpec.content == transition.toScene -> 1f
else -> 0f
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 74cd136b9911..0ac69124f7bc 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -81,12 +81,15 @@ sealed interface SceneTransitionLayoutState {
/**
* Whether we are transitioning. If [from] or [to] is empty, we will also check that they match
- * the scenes we are animating from and/or to.
+ * the contents we are animating from and/or to.
*/
- fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean
+ fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean
- /** Whether we are transitioning from [scene] to [other], or from [other] to [scene]. */
- fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean
+ /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
+ fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean
+
+ /** Whether we are transitioning from or to [content]. */
+ fun isTransitioningFromOrTo(content: ContentKey): Boolean
}
/** A [SceneTransitionLayoutState] whose target scene can be imperatively set. */
@@ -260,14 +263,19 @@ internal class MutableSceneTransitionLayoutStateImpl(
}
}
- override fun isTransitioning(from: SceneKey?, to: SceneKey?): Boolean {
+ override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean {
val transition = currentTransition ?: return false
return transition.isTransitioning(from, to)
}
- override fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean {
+ override fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
val transition = currentTransition ?: return false
- return transition.isTransitioningBetween(scene, other)
+ return transition.isTransitioningBetween(content, other)
+ }
+
+ override fun isTransitioningFromOrTo(content: ContentKey): Boolean {
+ val transition = currentTransition ?: return false
+ return transition.isTransitioningFromOrTo(content)
}
override fun setTargetScene(
@@ -293,10 +301,7 @@ internal class MutableSceneTransitionLayoutStateImpl(
*
* Important: you *must* call [finishTransition] once the transition is finished.
*/
- internal fun startTransition(
- transition: TransitionState.Transition.ChangeCurrentScene,
- chain: Boolean = true,
- ) {
+ internal fun startTransition(transition: TransitionState.Transition, chain: Boolean = true) {
checkThread()
// Set the current scene and overlays on the transition.
@@ -305,23 +310,23 @@ internal class MutableSceneTransitionLayoutStateImpl(
transition.currentOverlaysWhenTransitionStarted = currentState.currentOverlays
// Compute the [TransformationSpec] when the transition starts.
- val fromScene = transition.fromScene
- val toScene = transition.toScene
+ val fromContent = transition.fromContent
+ val toContent = transition.toContent
val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
// Update the transition specs.
transition.transformationSpec =
transitions
- .transitionSpec(fromScene, toScene, key = transition.key)
+ .transitionSpec(fromContent, toContent, key = transition.key)
.transformationSpec()
transition.previewTransformationSpec =
transitions
- .transitionSpec(fromScene, toScene, key = transition.key)
+ .transitionSpec(fromContent, toContent, key = transition.key)
.previewTransformationSpec()
if (orientation != null) {
transition.updateOverscrollSpecs(
- fromSpec = transitions.overscrollSpec(fromScene, orientation),
- toSpec = transitions.overscrollSpec(toScene, orientation),
+ fromSpec = transitions.overscrollSpec(fromContent, orientation),
+ toSpec = transitions.overscrollSpec(toContent, orientation),
)
} else {
transition.updateOverscrollSpecs(fromSpec = null, toSpec = null)
@@ -402,32 +407,27 @@ internal class MutableSceneTransitionLayoutStateImpl(
}
private fun setupTransitionLinks(transition: TransitionState.Transition) {
- when (transition) {
- is TransitionState.Transition.ChangeCurrentScene -> {
- stateLinks.fastForEach { stateLink ->
- val matchingLinks =
- stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
- if (matchingLinks.isEmpty()) return@fastForEach
- if (matchingLinks.size > 1) error("More than one link matched.")
-
- val targetCurrentScene = stateLink.target.transitionState.currentScene
- val matchingLink = matchingLinks[0]
-
- if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
-
- val linkedTransition =
- LinkedTransition(
- originalTransition = transition,
- fromScene = targetCurrentScene,
- toScene = matchingLink.targetTo,
- key = matchingLink.targetTransitionKey,
- )
-
- stateLink.target.startTransition(linkedTransition)
- transition.activeTransitionLinks[stateLink] = linkedTransition
- }
- }
- else -> error("transition links are not supported with overlays yet")
+ stateLinks.fastForEach { stateLink ->
+ val matchingLinks =
+ stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
+ if (matchingLinks.isEmpty()) return@fastForEach
+ if (matchingLinks.size > 1) error("More than one link matched.")
+
+ val targetCurrentScene = stateLink.target.transitionState.currentScene
+ val matchingLink = matchingLinks[0]
+
+ if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
+
+ val linkedTransition =
+ LinkedTransition(
+ originalTransition = transition,
+ fromScene = targetCurrentScene,
+ toScene = matchingLink.targetTo,
+ key = matchingLink.targetTransitionKey,
+ )
+
+ stateLink.target.startTransition(linkedTransition)
+ transition.activeTransitionLinks[stateLink] = linkedTransition
}
}
@@ -552,12 +552,39 @@ internal class MutableSceneTransitionLayoutStateImpl(
checkThread()
// Overlay is already shown, do nothing.
- if (overlay in transitionState.currentOverlays) {
+ val currentState = transitionState
+ if (overlay in currentState.currentOverlays) {
return
}
- // TODO(b/353679003): Animate the overlay instead of instantly snapping to an Idle state.
- snapToScene(transitionState.currentScene, transitionState.currentOverlays + overlay)
+ val fromScene = currentState.currentScene
+ fun animate(
+ replacedTransition: TransitionState.Transition.ShowOrHideOverlay? = null,
+ reversed: Boolean = false,
+ ) {
+ animationScope.showOrHideOverlay(
+ layoutState = this@MutableSceneTransitionLayoutStateImpl,
+ overlay = overlay,
+ fromOrToScene = fromScene,
+ isShowing = true,
+ transitionKey = transitionKey,
+ replacedTransition = replacedTransition,
+ reversed = reversed,
+ )
+ }
+
+ if (
+ currentState is TransitionState.Transition.ShowOrHideOverlay &&
+ currentState.overlay == overlay &&
+ currentState.fromOrToScene == fromScene
+ ) {
+ animate(
+ replacedTransition = currentState,
+ reversed = overlay == currentState.fromContent
+ )
+ } else {
+ animate()
+ }
}
override fun hideOverlay(
@@ -568,12 +595,36 @@ internal class MutableSceneTransitionLayoutStateImpl(
checkThread()
// Overlay is not shown, do nothing.
- if (!transitionState.currentOverlays.contains(overlay)) {
+ val currentState = transitionState
+ if (!currentState.currentOverlays.contains(overlay)) {
return
}
- // TODO(b/353679003): Animate the overlay instead of instantly snapping to an Idle state.
- snapToScene(transitionState.currentScene, transitionState.currentOverlays - overlay)
+ val toScene = currentState.currentScene
+ fun animate(
+ replacedTransition: TransitionState.Transition.ShowOrHideOverlay? = null,
+ reversed: Boolean = false,
+ ) {
+ animationScope.showOrHideOverlay(
+ layoutState = this@MutableSceneTransitionLayoutStateImpl,
+ overlay = overlay,
+ fromOrToScene = toScene,
+ isShowing = false,
+ transitionKey = transitionKey,
+ replacedTransition = replacedTransition,
+ reversed = reversed,
+ )
+ }
+
+ if (
+ currentState is TransitionState.Transition.ShowOrHideOverlay &&
+ currentState.overlay == overlay &&
+ currentState.fromOrToScene == toScene
+ ) {
+ animate(replacedTransition = currentState, reversed = overlay == currentState.toContent)
+ } else {
+ animate()
+ }
}
override fun replaceOverlay(
@@ -583,16 +634,45 @@ internal class MutableSceneTransitionLayoutStateImpl(
transitionKey: TransitionKey?
) {
checkThread()
- require(from in currentOverlays) {
+
+ val currentState = transitionState
+ require(from != to) {
+ "replaceOverlay must be called with different overlays (from = to = ${from.debugName})"
+ }
+ require(from in currentState.currentOverlays) {
"Overlay ${from.debugName} is not shown so it can't be replaced by ${to.debugName}"
}
- require(to !in currentOverlays) {
+ require(to !in currentState.currentOverlays) {
"Overlay ${to.debugName} is already shown so it can't replace ${from.debugName}"
}
- // TODO(b/353679003): Animate from into to instead of hiding/showing the overlays
- // separately.
- snapToScene(transitionState.currentScene, transitionState.currentOverlays - from + to)
+ fun animate(
+ replacedTransition: TransitionState.Transition.ReplaceOverlay? = null,
+ reversed: Boolean = false,
+ ) {
+ animationScope.replaceOverlay(
+ layoutState = this@MutableSceneTransitionLayoutStateImpl,
+ fromOverlay = if (reversed) to else from,
+ toOverlay = if (reversed) from else to,
+ transitionKey = transitionKey,
+ replacedTransition = replacedTransition,
+ reversed = reversed,
+ )
+ }
+
+ if (currentState is TransitionState.Transition.ReplaceOverlay) {
+ if (currentState.fromOverlay == from && currentState.toOverlay == to) {
+ animate(replacedTransition = currentState, reversed = false)
+ return
+ }
+
+ if (currentState.fromOverlay == to && currentState.toOverlay == from) {
+ animate(replacedTransition = currentState, reversed = true)
+ return
+ }
+ }
+
+ animate()
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index d35d95685d22..cefcff75f13a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -49,16 +49,16 @@ internal constructor(
) {
private val transitionCache =
mutableMapOf<
- SceneKey,
- MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>>
+ ContentKey,
+ MutableMap<ContentKey, MutableMap<TransitionKey?, TransitionSpecImpl>>
>()
private val overscrollCache =
- mutableMapOf<SceneKey, MutableMap<Orientation, OverscrollSpecImpl?>>()
+ mutableMapOf<ContentKey, MutableMap<Orientation, OverscrollSpecImpl?>>()
internal fun transitionSpec(
- from: SceneKey,
- to: SceneKey,
+ from: ContentKey,
+ to: ContentKey,
key: TransitionKey?,
): TransitionSpecImpl {
return transitionCache
@@ -67,7 +67,11 @@ internal constructor(
.getOrPut(key) { findSpec(from, to, key) }
}
- private fun findSpec(from: SceneKey, to: SceneKey, key: TransitionKey?): TransitionSpecImpl {
+ private fun findSpec(
+ from: ContentKey,
+ to: ContentKey,
+ key: TransitionKey?
+ ): TransitionSpecImpl {
val spec = transition(from, to, key) { it.from == from && it.to == to }
if (spec != null) {
return spec
@@ -93,8 +97,8 @@ internal constructor(
}
private fun transition(
- from: SceneKey,
- to: SceneKey,
+ from: ContentKey,
+ to: ContentKey,
key: TransitionKey?,
filter: (TransitionSpecImpl) -> Boolean,
): TransitionSpecImpl? {
@@ -110,16 +114,16 @@ internal constructor(
return match
}
- private fun defaultTransition(from: SceneKey, to: SceneKey) =
+ private fun defaultTransition(from: ContentKey, to: ContentKey) =
TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider)
- internal fun overscrollSpec(scene: SceneKey, orientation: Orientation): OverscrollSpecImpl? =
+ internal fun overscrollSpec(scene: ContentKey, orientation: Orientation): OverscrollSpecImpl? =
overscrollCache
.getOrPut(scene) { mutableMapOf() }
- .getOrPut(orientation) { overscroll(scene, orientation) { it.scene == scene } }
+ .getOrPut(orientation) { overscroll(scene, orientation) { it.content == scene } }
private fun overscroll(
- scene: SceneKey,
+ scene: ContentKey,
orientation: Orientation,
filter: (OverscrollSpecImpl) -> Boolean,
): OverscrollSpecImpl? {
@@ -264,10 +268,10 @@ internal class TransitionSpecImpl(
previewTransformationSpec?.invoke()
}
-/** The definition of the overscroll behavior of the [scene]. */
+/** The definition of the overscroll behavior of the [content]. */
interface OverscrollSpec {
/** The scene we are over scrolling. */
- val scene: SceneKey
+ val content: ContentKey
/** The orientation of this [OverscrollSpec]. */
val orientation: Orientation
@@ -288,7 +292,7 @@ interface OverscrollSpec {
}
internal class OverscrollSpecImpl(
- override val scene: SceneKey,
+ override val content: ContentKey,
override val orientation: Orientation,
override val transformationSpec: TransformationSpecImpl,
override val progressConverter: ProgressConverter?,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 523e5bdd7203..18e356f71768 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -107,7 +107,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
): OverscrollSpec {
val spec =
OverscrollSpecImpl(
- scene = scene,
+ content = scene,
orientation = orientation,
transformationSpec =
TransformationSpecImpl(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 6f608cbc1d7f..6bc1754150fe 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -96,6 +96,8 @@ internal sealed class Content(
.approachLayout(
isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
) { measurable, constraints ->
+ // TODO(b/353679003): Use the ModifierNode API to set this *before* the approach
+ // pass is started.
targetSize = lookaheadSize
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
index 89b004046475..59ddb1354073 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
@@ -23,7 +23,7 @@ import kotlinx.coroutines.Job
/** A linked transition which is driven by a [originalTransition]. */
internal class LinkedTransition(
- private val originalTransition: TransitionState.Transition.ChangeCurrentScene,
+ private val originalTransition: TransitionState.Transition,
fromScene: SceneKey,
toScene: SceneKey,
override val key: TransitionKey? = null,
@@ -32,8 +32,8 @@ internal class LinkedTransition(
override val currentScene: SceneKey
get() {
return when (originalTransition.currentScene) {
- originalTransition.fromScene -> fromScene
- originalTransition.toScene -> toScene
+ originalTransition.fromContent -> fromScene
+ originalTransition.toContent -> toScene
else -> error("Original currentScene is neither FromScene nor ToScene")
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
index c29bf212ec9c..c830ca4fa7c0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene.transition.link
+import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutState
@@ -35,8 +36,8 @@ class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<Tr
* target to `SceneA` from any current scene.
*/
class TransitionLink(
- val sourceFrom: SceneKey?,
- val sourceTo: SceneKey?,
+ val sourceFrom: ContentKey?,
+ val sourceTo: ContentKey?,
val targetFrom: SceneKey?,
val targetTo: SceneKey,
val targetTransitionKey: TransitionKey? = null,
@@ -50,15 +51,15 @@ class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<Tr
}
internal fun isMatchingLink(
- transition: TransitionState.Transition.ChangeCurrentScene,
+ transition: TransitionState.Transition,
): Boolean {
- return (sourceFrom == null || sourceFrom == transition.fromScene) &&
- (sourceTo == null || sourceTo == transition.toScene)
+ return (sourceFrom == null || sourceFrom == transition.fromContent) &&
+ (sourceTo == null || sourceTo == transition.toContent)
}
- internal fun targetIsInValidState(targetCurrentScene: SceneKey): Boolean {
- return (targetFrom == null || targetFrom == targetCurrentScene) &&
- targetTo != targetCurrentScene
+ internal fun targetIsInValidState(targetCurrentContent: ContentKey): Boolean {
+ return (targetFrom == null || targetFrom == targetCurrentContent) &&
+ targetTo != targetCurrentContent
}
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 25be3f97a2c4..7d8e898e9ab2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1020,7 +1020,7 @@ class DraggableHandlerTest {
// We scrolled down, under scene C there is nothing, so we can use the overscroll spec
assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull()
- assertThat(layoutState.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneC)
+ assertThat(layoutState.currentTransition?.currentOverscrollSpec?.content).isEqualTo(SceneC)
val transition = layoutState.currentTransition
assertThat(transition).isNotNull()
assertThat(transition!!.progress).isEqualTo(-0.1f)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index d4391e09fc3d..85db418f6020 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -16,6 +16,8 @@
package com.android.compose.animation.scene
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -33,6 +35,7 @@ import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+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.TestOverlays.OverlayA
@@ -49,8 +52,8 @@ class OverlayTest {
@get:Rule val rule = createComposeRule()
@Composable
- private fun ContentScope.Foo() {
- Box(Modifier.element(TestElements.Foo).size(100.dp))
+ private fun ContentScope.Foo(width: Dp = 100.dp, height: Dp = 100.dp) {
+ Box(Modifier.element(TestElements.Foo).size(width, height))
}
@Test
@@ -316,4 +319,209 @@ class OverlayTest {
// of the layout.
rule.onNodeWithTag(contentTag).assertSizeIsEqualTo(100.dp)
}
+
+ @Test
+ fun showAnimation() {
+ rule.testShowOverlayTransition(
+ fromSceneContent = {
+ Box(Modifier.size(width = 180.dp, height = 120.dp)) {
+ Foo(width = 60.dp, height = 40.dp)
+ }
+ },
+ overlayContent = { Foo(width = 100.dp, height = 80.dp) },
+ transition = {
+ // 4 frames of animation
+ spec = tween(4 * 16, easing = LinearEasing)
+ },
+ ) {
+ // Foo moves from (0,0) with a size of 60x40dp to centered (in a 180x120dp Box) with a
+ // size of 100x80dp, so at (40,20).
+ before {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertSizeIsEqualTo(60.dp, 40.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ }
+
+ at(16) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(70.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(10.dp, 5.dp)
+ }
+
+ at(32) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(80.dp, 60.dp)
+ .assertPositionInRootIsEqualTo(20.dp, 10.dp)
+ }
+
+ at(48) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(90.dp, 70.dp)
+ .assertPositionInRootIsEqualTo(30.dp, 15.dp)
+ }
+
+ after {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp, 80.dp)
+ .assertPositionInRootIsEqualTo(40.dp, 20.dp)
+ }
+ }
+ }
+
+ @Test
+ fun hideAnimation() {
+ rule.testHideOverlayTransition(
+ toSceneContent = {
+ Box(Modifier.size(width = 180.dp, height = 120.dp)) {
+ Foo(width = 60.dp, height = 40.dp)
+ }
+ },
+ overlayContent = { Foo(width = 100.dp, height = 80.dp) },
+ transition = {
+ // 4 frames of animation
+ spec = tween(4 * 16, easing = LinearEasing)
+ },
+ ) {
+ // Foo moves from centered (in a 180x120dp Box) with a size of 100x80dp, so at (40,20),
+ // to (0,0) with a size of 60x40dp.
+ before {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(100.dp, 80.dp)
+ .assertPositionInRootIsEqualTo(40.dp, 20.dp)
+ }
+
+ at(16) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(90.dp, 70.dp)
+ .assertPositionInRootIsEqualTo(30.dp, 15.dp)
+ }
+
+ at(32) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(80.dp, 60.dp)
+ .assertPositionInRootIsEqualTo(20.dp, 10.dp)
+ }
+
+ at(48) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(70.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(10.dp, 5.dp)
+ }
+
+ after {
+ rule
+ .onNode(isElement(TestElements.Foo, content = SceneA))
+ .assertSizeIsEqualTo(60.dp, 40.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ }
+ }
+ }
+
+ @Test
+ fun replaceAnimation() {
+ rule.testReplaceOverlayTransition(
+ currentSceneContent = { Box(Modifier.size(width = 180.dp, height = 120.dp)) },
+ fromContent = { Foo(width = 60.dp, height = 40.dp) },
+ fromAlignment = Alignment.TopStart,
+ toContent = { Foo(width = 100.dp, height = 80.dp) },
+ transition = {
+ // 4 frames of animation
+ spec = tween(4 * 16, easing = LinearEasing)
+ },
+ ) {
+ // Foo moves from (0,0) with a size of 60x40dp to centered (in a 180x120dp Box) with a
+ // size of 100x80dp, so at (40,20).
+ before {
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertSizeIsEqualTo(60.dp, 40.dp)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist()
+ }
+
+ at(16) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayB))
+ .assertSizeIsEqualTo(70.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(10.dp, 5.dp)
+ }
+
+ at(32) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayB))
+ .assertSizeIsEqualTo(80.dp, 60.dp)
+ .assertPositionInRootIsEqualTo(20.dp, 10.dp)
+ }
+
+ at(48) {
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayA))
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayB))
+ .assertSizeIsEqualTo(90.dp, 70.dp)
+ .assertPositionInRootIsEqualTo(30.dp, 15.dp)
+ }
+
+ after {
+ rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist()
+ rule
+ .onNode(isElement(TestElements.Foo, content = OverlayB))
+ .assertSizeIsEqualTo(100.dp, 80.dp)
+ .assertPositionInRootIsEqualTo(40.dp, 20.dp)
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 3422a8e47a3d..69f2cbace276 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -495,7 +495,7 @@ class SceneTransitionLayoutStateTest {
// overscroll for SceneB is defined
progress.value = 1.1f
val overscrollSpec = assertThat(transition).hasOverscrollSpec()
- assertThat(overscrollSpec.scene).isEqualTo(SceneB)
+ assertThat(overscrollSpec.content).isEqualTo(SceneB)
}
@Test
@@ -516,7 +516,7 @@ class SceneTransitionLayoutStateTest {
// overscroll for SceneA is defined
progress.value = -0.1f
val overscrollSpec = assertThat(transition).hasOverscrollSpec()
- assertThat(overscrollSpec.scene).isEqualTo(SceneA)
+ assertThat(overscrollSpec.content).isEqualTo(SceneA)
// scroll from SceneA to SceneB
progress.value = 0.5f
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 7f26b9855a9a..1ebd3d98471b 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
@@ -16,9 +16,12 @@
package com.android.compose.animation.scene
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -115,6 +118,97 @@ fun ComposeContentTestRule.testTransition(
)
}
+/** Test the transition when showing [overlay] from [fromScene]. */
+fun ComposeContentTestRule.testShowOverlayTransition(
+ fromSceneContent: @Composable ContentScope.() -> Unit,
+ overlayContent: @Composable ContentScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ fromScene: SceneKey = TestScenes.SceneA,
+ overlay: OverlayKey = TestOverlays.OverlayA,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ testTransition(
+ state =
+ runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ fromScene,
+ transitions = transitions { from(fromScene, overlay, builder = transition) },
+ )
+ },
+ transitionLayout = { state ->
+ SceneTransitionLayout(state) {
+ scene(fromScene) { fromSceneContent() }
+ overlay(overlay) { overlayContent() }
+ }
+ },
+ changeState = { state -> state.showOverlay(overlay, animationScope = this) },
+ builder = builder,
+ )
+}
+
+/** Test the transition when hiding [overlay] to [toScene]. */
+fun ComposeContentTestRule.testHideOverlayTransition(
+ toSceneContent: @Composable ContentScope.() -> Unit,
+ overlayContent: @Composable ContentScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ toScene: SceneKey = TestScenes.SceneA,
+ overlay: OverlayKey = TestOverlays.OverlayA,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ testTransition(
+ state =
+ runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ toScene,
+ initialOverlays = setOf(overlay),
+ transitions = transitions { from(overlay, toScene, builder = transition) },
+ )
+ },
+ transitionLayout = { state ->
+ SceneTransitionLayout(state) {
+ scene(toScene) { toSceneContent() }
+ overlay(overlay) { overlayContent() }
+ }
+ },
+ changeState = { state -> state.hideOverlay(overlay, animationScope = this) },
+ builder = builder,
+ )
+}
+
+/** Test the transition when replace [from] to [to]. */
+fun ComposeContentTestRule.testReplaceOverlayTransition(
+ fromContent: @Composable ContentScope.() -> Unit,
+ toContent: @Composable ContentScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ currentSceneContent: @Composable ContentScope.() -> Unit = { Box(Modifier.fillMaxSize()) },
+ fromAlignment: Alignment = Alignment.Center,
+ toAlignment: Alignment = Alignment.Center,
+ from: OverlayKey = TestOverlays.OverlayA,
+ to: OverlayKey = TestOverlays.OverlayB,
+ currentScene: SceneKey = TestScenes.SceneA,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ testTransition(
+ state =
+ runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ currentScene,
+ initialOverlays = setOf(from),
+ transitions = transitions { from(from, to, builder = transition) },
+ )
+ },
+ transitionLayout = { state ->
+ SceneTransitionLayout(state) {
+ scene(currentScene) { currentSceneContent() }
+ overlay(from, fromAlignment) { fromContent() }
+ overlay(to, toAlignment) { toContent() }
+ }
+ },
+ changeState = { state -> state.replaceOverlay(from, to, animationScope = this) },
+ builder = builder,
+ )
+}
+
data class TransitionRecordingSpec(
val recordBefore: Boolean = true,
val recordAfter: Boolean = true,
@@ -188,6 +282,21 @@ fun ComposeContentTestRule.testTransition(
"(${currentScene.debugName})"
}
+ testTransition(
+ state = state,
+ changeState = { state -> state.setTargetScene(to, coroutineScope = this) },
+ transitionLayout = transitionLayout,
+ builder = builder,
+ )
+}
+
+/** Test the transition from [state] to [to]. */
+fun ComposeContentTestRule.testTransition(
+ state: MutableSceneTransitionLayoutState,
+ changeState: CoroutineScope.(MutableSceneTransitionLayoutState) -> Unit,
+ transitionLayout: @Composable (state: MutableSceneTransitionLayoutState) -> Unit,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
val test = transitionTest(builder)
val assertionScope =
object : TransitionTestAssertionScope {
@@ -213,7 +322,7 @@ fun ComposeContentTestRule.testTransition(
mainClock.autoAdvance = false
// Change the current scene.
- runOnUiThread { state.setTargetScene(to, coroutineScope) }
+ runOnUiThread { coroutineScope.changeState(state) }
waitForIdle()
mainClock.advanceTimeByFrame()
waitForIdle()