summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt184
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt36
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt86
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt17
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt13
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt26
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt104
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt112
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt3
11 files changed, 514 insertions, 73 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 848507971c66..dc89f4548428 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.content.Content
+import com.android.compose.animation.scene.content.Overlay
import com.android.compose.animation.scene.content.Scene
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
@@ -165,7 +166,7 @@ internal class DraggableHandlerImpl(
}
val swipes = computeSwipes(startedPosition, pointersDown)
- val fromContent = layoutImpl.scene(layoutImpl.state.currentScene)
+ val fromContent = layoutImpl.contentForUserActions()
val result =
swipes.findUserActionResult(fromContent, overSlop, updateSwipesResults = true)
// As we were unable to locate a valid target scene, the initial SwipeAnimation
@@ -199,21 +200,66 @@ internal class DraggableHandlerImpl(
else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
}
+ fun <T : Content> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> {
+ return SwipeAnimation(
+ layoutImpl = layoutImpl,
+ fromContent = fromContent,
+ toContent = toContent,
+ userActionDistanceScope = layoutImpl.userActionDistanceScope,
+ orientation = orientation,
+ isUpOrLeft = isUpOrLeft,
+ requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
+ )
+ }
+
val layoutState = layoutImpl.state
return when (result) {
is UserActionResult.ChangeScene -> {
+ val fromScene = layoutImpl.scene(layoutState.currentScene)
+ val toScene = layoutImpl.scene(result.toScene)
ChangeCurrentSceneSwipeTransition(
layoutState = layoutState,
swipeAnimation =
- SwipeAnimation(
- layoutImpl = layoutImpl,
- fromContent = layoutImpl.scene(layoutState.currentScene),
- toContent = layoutImpl.scene(result.toScene),
- userActionDistanceScope = layoutImpl.userActionDistanceScope,
- orientation = orientation,
- isUpOrLeft = isUpOrLeft,
- requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
- ),
+ swipeAnimation(fromContent = fromScene, toContent = toScene),
+ key = result.transitionKey,
+ replacedTransition = null,
+ )
+ .swipeAnimation
+ }
+ is UserActionResult.ShowOverlay -> {
+ val fromScene = layoutImpl.scene(layoutState.currentScene)
+ val overlay = layoutImpl.overlay(result.overlay)
+ ShowOrHideOverlaySwipeTransition(
+ layoutState = layoutState,
+ _fromOrToScene = fromScene,
+ _overlay = overlay,
+ swipeAnimation =
+ swipeAnimation(fromContent = fromScene, toContent = overlay),
+ key = result.transitionKey,
+ replacedTransition = null,
+ )
+ .swipeAnimation
+ }
+ is UserActionResult.HideOverlay -> {
+ val toScene = layoutImpl.scene(layoutState.currentScene)
+ val overlay = layoutImpl.overlay(result.overlay)
+ ShowOrHideOverlaySwipeTransition(
+ layoutState = layoutState,
+ _fromOrToScene = toScene,
+ _overlay = overlay,
+ swipeAnimation = swipeAnimation(fromContent = overlay, toContent = toScene),
+ key = result.transitionKey,
+ replacedTransition = null,
+ )
+ .swipeAnimation
+ }
+ is UserActionResult.ReplaceByOverlay -> {
+ val fromOverlay = layoutImpl.contentForUserActions() as Overlay
+ val toOverlay = layoutImpl.overlay(result.overlay)
+ ReplaceOverlaySwipeTransition(
+ layoutState = layoutState,
+ swipeAnimation =
+ swipeAnimation(fromContent = fromOverlay, toContent = toOverlay),
key = result.transitionKey,
replacedTransition = null,
)
@@ -228,8 +274,14 @@ internal class DraggableHandlerImpl(
ChangeCurrentSceneSwipeTransition(transition as ChangeCurrentSceneSwipeTransition)
.swipeAnimation
}
- is TransitionState.Transition.OverlayTransition ->
- TODO("b/353679003: Support overlay transitions")
+ is TransitionState.Transition.ShowOrHideOverlay -> {
+ ShowOrHideOverlaySwipeTransition(transition as ShowOrHideOverlaySwipeTransition)
+ .swipeAnimation
+ }
+ is TransitionState.Transition.ReplaceOverlay -> {
+ ReplaceOverlaySwipeTransition(transition as ReplaceOverlaySwipeTransition)
+ .swipeAnimation
+ }
}
}
@@ -495,11 +547,23 @@ private class DragControllerImpl(
}
fun shouldChangeContent(): Boolean {
- return when (swipeAnimation.contentTransition) {
+ return when (val transition = swipeAnimation.contentTransition) {
is TransitionState.Transition.ChangeCurrentScene ->
layoutState.canChangeScene(targetContent.key as SceneKey)
- is TransitionState.Transition.OverlayTransition ->
- TODO("b/353679003: Support overlay transitions")
+ is TransitionState.Transition.ShowOrHideOverlay -> {
+ if (targetContent.key == transition.overlay) {
+ layoutState.canShowOverlay(transition.overlay)
+ } else {
+ layoutState.canHideOverlay(transition.overlay)
+ }
+ }
+ is TransitionState.Transition.ReplaceOverlay -> {
+ val to = targetContent.key as OverlayKey
+ val from =
+ if (to == transition.toOverlay) transition.fromOverlay
+ else transition.toOverlay
+ layoutState.canReplaceOverlay(from, to)
+ }
}
}
@@ -618,6 +682,96 @@ private class ChangeCurrentSceneSwipeTransition(
override fun finish(): Job = swipeAnimation.finish()
}
+private class ShowOrHideOverlaySwipeTransition(
+ val layoutState: MutableSceneTransitionLayoutStateImpl,
+ val swipeAnimation: SwipeAnimation<Content>,
+ val _overlay: Overlay,
+ val _fromOrToScene: Scene,
+ override val key: TransitionKey?,
+ replacedTransition: ShowOrHideOverlaySwipeTransition?,
+) :
+ TransitionState.Transition.ShowOrHideOverlay(
+ _overlay.key,
+ _fromOrToScene.key,
+ swipeAnimation.fromContent.key,
+ swipeAnimation.toContent.key,
+ replacedTransition,
+ ),
+ TransitionState.HasOverscrollProperties by swipeAnimation {
+ constructor(
+ other: ShowOrHideOverlaySwipeTransition
+ ) : this(
+ layoutState = other.layoutState,
+ swipeAnimation = SwipeAnimation(other.swipeAnimation),
+ _overlay = other._overlay,
+ _fromOrToScene = other._fromOrToScene,
+ key = other.key,
+ replacedTransition = other,
+ )
+
+ init {
+ swipeAnimation.contentTransition = this
+ }
+
+ override val isEffectivelyShown: Boolean
+ get() = swipeAnimation.currentContent == _overlay
+
+ override val progress: Float
+ get() = swipeAnimation.progress
+
+ override val progressVelocity: Float
+ get() = swipeAnimation.progressVelocity
+
+ override val isInitiatedByUserInput: Boolean = true
+
+ override val isUserInputOngoing: Boolean
+ get() = swipeAnimation.isUserInputOngoing
+
+ override fun finish(): Job = swipeAnimation.finish()
+}
+
+private class ReplaceOverlaySwipeTransition(
+ val layoutState: MutableSceneTransitionLayoutStateImpl,
+ val swipeAnimation: SwipeAnimation<Overlay>,
+ override val key: TransitionKey?,
+ replacedTransition: ReplaceOverlaySwipeTransition?,
+) :
+ TransitionState.Transition.ReplaceOverlay(
+ swipeAnimation.fromContent.key,
+ swipeAnimation.toContent.key,
+ replacedTransition,
+ ),
+ TransitionState.HasOverscrollProperties by swipeAnimation {
+ constructor(
+ other: ReplaceOverlaySwipeTransition
+ ) : this(
+ layoutState = other.layoutState,
+ swipeAnimation = SwipeAnimation(other.swipeAnimation),
+ key = other.key,
+ replacedTransition = other,
+ )
+
+ init {
+ swipeAnimation.contentTransition = this
+ }
+
+ override val effectivelyShownOverlay: OverlayKey
+ get() = swipeAnimation.currentContent.key
+
+ override val progress: Float
+ get() = swipeAnimation.progress
+
+ override val progressVelocity: Float
+ get() = swipeAnimation.progressVelocity
+
+ override val isInitiatedByUserInput: Boolean = true
+
+ override val isUserInputOngoing: Boolean
+ get() = swipeAnimation.isUserInputOngoing
+
+ override fun finish(): Job = swipeAnimation.finish()
+}
+
/** A helper class that contains the main logic for swipe transitions. */
internal class SwipeAnimation<T : Content>(
val layoutImpl: SceneTransitionLayoutImpl,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 43c95e7fd9f7..b3f74f749a0e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -104,10 +104,10 @@ interface SceneTransitionLayoutScope {
* call order. Calling overlay(A) followed by overlay(B) will mean that overlay B renders
* after/above overlay A.
*/
- // TODO(b/353679003): Allow to specify user actions. When overlays are shown, the user actions
- // of the top-most overlay in currentOverlays will be used.
fun overlay(
key: OverlayKey,
+ userActions: Map<UserAction, UserActionResult> =
+ mapOf(Back to UserActionResult.HideOverlay(key)),
alignment: Alignment = Alignment.Center,
content: @Composable ContentScope.() -> Unit,
)
@@ -502,6 +502,38 @@ sealed class UserActionResult(
override fun toContent(currentScene: SceneKey): ContentKey = toScene
}
+ /** A [UserActionResult] that shows [overlay]. */
+ class ShowOverlay(
+ val overlay: OverlayKey,
+ transitionKey: TransitionKey? = null,
+ requiresFullDistanceSwipe: Boolean = false,
+ ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
+ override fun toContent(currentScene: SceneKey): ContentKey = overlay
+ }
+
+ /** A [UserActionResult] that hides [overlay]. */
+ class HideOverlay(
+ val overlay: OverlayKey,
+ transitionKey: TransitionKey? = null,
+ requiresFullDistanceSwipe: Boolean = false,
+ ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
+ override fun toContent(currentScene: SceneKey): ContentKey = currentScene
+ }
+
+ /**
+ * A [UserActionResult] that replaces the current overlay by [overlay].
+ *
+ * Note: This result can only be used for user actions of overlays and an exception will be
+ * thrown if it is used for a scene.
+ */
+ class ReplaceByOverlay(
+ val overlay: OverlayKey,
+ transitionKey: TransitionKey? = null,
+ requiresFullDistanceSwipe: Boolean = false,
+ ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
+ override fun toContent(currentScene: SceneKey): ContentKey = overlay
+ }
+
companion object {
/** A [UserActionResult] that changes the current scene to [toScene]. */
operator fun invoke(
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 81980ba33ae5..258be8122c1d 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
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -182,6 +183,28 @@ internal class SceneTransitionLayoutImpl(
}
}
+ internal fun contentForUserActions(): Content {
+ return findOverlayWithHighestZIndex() ?: scene(state.transitionState.currentScene)
+ }
+
+ private fun findOverlayWithHighestZIndex(): Overlay? {
+ val currentOverlays = state.transitionState.currentOverlays
+ if (currentOverlays.isEmpty()) {
+ return null
+ }
+
+ var overlay: Overlay? = null
+ currentOverlays.forEach { key ->
+ val previousZIndex = overlay?.zIndex
+ val candidate = overlay(key)
+ if (previousZIndex == null || candidate.zIndex > previousZIndex) {
+ overlay = candidate
+ }
+ }
+
+ return overlay
+ }
+
internal fun updateContents(
builder: SceneTransitionLayoutScope.() -> Unit,
layoutDirection: LayoutDirection,
@@ -206,8 +229,7 @@ internal class SceneTransitionLayoutImpl(
scenesToRemove.remove(key)
- val resolvedUserActions =
- userActions.mapKeys { it.key.resolve(layoutDirection) }
+ val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection)
val scene = scenes[key]
if (scene != null) {
// Update an existing scene.
@@ -231,6 +253,7 @@ internal class SceneTransitionLayoutImpl(
override fun overlay(
key: OverlayKey,
+ userActions: Map<UserAction, UserActionResult>,
alignment: Alignment,
content: @Composable (ContentScope.() -> Unit)
) {
@@ -238,10 +261,12 @@ internal class SceneTransitionLayoutImpl(
overlaysToRemove.remove(key)
val overlay = overlays[key]
+ val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection)
if (overlay != null) {
// Update an existing overlay.
overlay.content = content
overlay.zIndex = zIndex
+ overlay.userActions = resolvedUserActions
overlay.alignment = alignment
} else {
// New overlay.
@@ -250,8 +275,7 @@ internal class SceneTransitionLayoutImpl(
key,
this@SceneTransitionLayoutImpl,
content,
- // TODO(b/353679003): Allow to specify user actions
- actions = emptyMap(),
+ resolvedUserActions,
zIndex,
alignment,
)
@@ -266,6 +290,46 @@ internal class SceneTransitionLayoutImpl(
overlaysToRemove.forEach { overlays.remove(it) }
}
+ private fun resolveUserActions(
+ key: ContentKey,
+ userActions: Map<UserAction, UserActionResult>,
+ layoutDirection: LayoutDirection
+ ): Map<UserAction.Resolved, UserActionResult> {
+ return userActions
+ .mapKeys { it.key.resolve(layoutDirection) }
+ .also { checkUserActions(key, it) }
+ }
+
+ private fun checkUserActions(
+ key: ContentKey,
+ userActions: Map<UserAction.Resolved, UserActionResult>,
+ ) {
+ userActions.forEach { (action, result) ->
+ fun details() = "Content $key, action $action, result $result."
+
+ when (result) {
+ is UserActionResult.ChangeScene -> {
+ check(key != result.toScene) {
+ error("Transition to the same scene is not supported. ${details()}")
+ }
+ }
+ is UserActionResult.ReplaceByOverlay -> {
+ check(key is OverlayKey) {
+ "ReplaceByOverlay() can only be used for overlays, not scenes. ${details()}"
+ }
+
+ check(key != result.overlay) {
+ "Transition to the same overlay is not supported. ${details()}"
+ }
+ }
+ is UserActionResult.ShowOverlay,
+ is UserActionResult.HideOverlay -> {
+ /* Always valid. */
+ }
+ }
+ }
+ }
+
@Composable
internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) {
Box(
@@ -289,11 +353,16 @@ internal class SceneTransitionLayoutImpl(
@Composable
private fun BackHandler() {
- val result = scene(state.transitionState.currentScene).userActions[Back.Resolved]
val targetSceneForBack =
- when (result) {
+ when (val result = contentForUserActions().userActions[Back.Resolved]) {
null -> null
is UserActionResult.ChangeScene -> result.toScene
+ is UserActionResult.ShowOverlay,
+ is UserActionResult.HideOverlay,
+ is UserActionResult.ReplaceByOverlay -> {
+ // TODO(b/353679003): Support overlay transitions when going back
+ null
+ }
}
PredictiveBackHandler(state, coroutineScope, targetSceneForBack)
@@ -387,9 +456,10 @@ internal class SceneTransitionLayoutImpl(
.sortedBy { it.zIndex }
}
- internal fun setScenesAndLayoutTargetSizeForTest(size: IntSize) {
+ @VisibleForTesting
+ internal fun setContentsAndLayoutTargetSizeForTest(size: IntSize) {
lastSize = size
- scenes.values.forEach { it.targetSize = size }
+ (scenes.values + overlays.values).forEach { it.targetSize = size }
}
internal fun overlaysOrNullForTest(): Map<OverlayKey, Overlay>? = _overlays
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 0ac69124f7bc..47065c7581fc 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
@@ -183,6 +183,12 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState
* commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns
* `true`, then the gesture will be committed and we will animate to the other scene. Otherwise,
* the gesture will be cancelled and we will animate back to the current scene.
+ * @param canShowOverlay whether we should commit a user action that will result in showing the
+ * given overlay.
+ * @param canHideOverlay whether we should commit a user action that will result in hiding the given
+ * overlay.
+ * @param canReplaceOverlay whether we should commit a user action that will result in replacing
+ * `from` overlay by `to` overlay.
* @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other
* [SceneTransitionLayoutState]s.
*/
@@ -191,6 +197,9 @@ fun MutableSceneTransitionLayoutState(
transitions: SceneTransitions = SceneTransitions.Empty,
initialOverlays: Set<OverlayKey> = emptySet(),
canChangeScene: (SceneKey) -> Boolean = { true },
+ canShowOverlay: (OverlayKey) -> Boolean = { true },
+ canHideOverlay: (OverlayKey) -> Boolean = { true },
+ canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
stateLinks: List<StateLink> = emptyList(),
enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
): MutableSceneTransitionLayoutState {
@@ -199,6 +208,9 @@ fun MutableSceneTransitionLayoutState(
transitions,
initialOverlays,
canChangeScene,
+ canShowOverlay,
+ canHideOverlay,
+ canReplaceOverlay,
stateLinks,
enableInterruptions,
)
@@ -210,6 +222,11 @@ internal class MutableSceneTransitionLayoutStateImpl(
override var transitions: SceneTransitions = transitions {},
initialOverlays: Set<OverlayKey> = emptySet(),
internal val canChangeScene: (SceneKey) -> Boolean = { true },
+ internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
+ internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
+ internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
+ true
+ },
private val stateLinks: List<StateLink> = emptyList(),
// TODO(b/290930950): Remove this flag.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index d1e83bacf40a..dc7eda5b9cf6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -31,7 +31,7 @@ import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.unit.IntSize
-import com.android.compose.animation.scene.content.Scene
+import com.android.compose.animation.scene.content.Content
/**
* Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
@@ -126,16 +126,15 @@ private class SwipeToSceneNode(
private fun enabled(): Boolean {
return draggableHandler.isDrivingTransition ||
- currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation)
+ contentForSwipes().shouldEnableSwipes(multiPointerDraggableNode.orientation)
}
- private fun currentScene(): Scene {
- val layoutImpl = draggableHandler.layoutImpl
- return layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
+ private fun contentForSwipes(): Content {
+ return draggableHandler.layoutImpl.contentForUserActions()
}
/** Whether swipe should be enabled in the given [orientation]. */
- private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
+ private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
return userActions.keys.any {
it is Swipe.Resolved && it.direction.orientation == orientation
}
@@ -153,7 +152,7 @@ private class SwipeToSceneNode(
Orientation.Vertical -> Orientation.Horizontal
Orientation.Horizontal -> Orientation.Vertical
}
- return currentScene().shouldEnableSwipes(oppositeOrientation)
+ return contentForSwipes().shouldEnableSwipes(oppositeOrientation)
}
}
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 3bd2744175c8..59dd896ad9ea 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
@@ -66,31 +66,7 @@ internal sealed class Content(
var content by mutableStateOf(content)
var zIndex by mutableFloatStateOf(zIndex)
var targetSize by mutableStateOf(IntSize.Zero)
-
- private var _userActions by mutableStateOf(checkValid(actions))
- var userActions
- get() = _userActions
- set(value) {
- _userActions = checkValid(value)
- }
-
- private fun checkValid(
- userActions: Map<UserAction.Resolved, UserActionResult>
- ): Map<UserAction.Resolved, UserActionResult> {
- userActions.forEach { (action, result) ->
- when (result) {
- is UserActionResult.ChangeScene -> {
- if (key == result.toScene) {
- error(
- "Transition to the same content (scene/overlay) is not supported. " +
- "Content $key, action $action, result $result"
- )
- }
- }
- }
- }
- return userActions
- }
+ var userActions by mutableStateOf(actions)
@Composable
fun Content(modifier: Modifier = Modifier) {
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 38db222977df..9fa4722cf86f 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
@@ -31,6 +31,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.NestedScrollBehavior.EdgeAlways
import com.android.compose.animation.scene.NestedScrollBehavior.EdgeNoPreview
import com.android.compose.animation.scene.NestedScrollBehavior.EdgeWithPreview
+import com.android.compose.animation.scene.TestOverlays.OverlayA
+import com.android.compose.animation.scene.TestOverlays.OverlayB
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
@@ -52,7 +54,7 @@ private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
@RunWith(AndroidJUnit4::class)
class DraggableHandlerTest {
private class TestGestureScope(
- private val testScope: MonotonicClockTestScope,
+ val testScope: MonotonicClockTestScope,
) {
var canChangeScene: (SceneKey) -> Boolean = { true }
val layoutState =
@@ -103,6 +105,21 @@ class DraggableHandlerTest {
) {
Text("SceneC")
}
+ overlay(
+ key = OverlayA,
+ userActions =
+ mapOf(
+ Swipe.Up to UserActionResult.HideOverlay(OverlayA),
+ Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)
+ ),
+ ) {
+ Text("OverlayA")
+ }
+ overlay(
+ key = OverlayB,
+ ) {
+ Text("OverlayB")
+ }
}
val transitionInterceptionThreshold = 0.05f
@@ -117,7 +134,7 @@ class DraggableHandlerTest {
builder = scenesBuilder,
coroutineScope = testScope,
)
- .apply { setScenesAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
+ .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
@@ -1277,4 +1294,87 @@ class DraggableHandlerTest {
assertThat(newTransition).isNotSameInstanceAs(transition)
assertThat(newTransition.replacedTransition).isSameInstanceAs(transition)
}
+
+ @Test
+ fun showOverlay() = runGestureTest {
+ mutableUserActionsA = mapOf(Swipe.Down to UserActionResult.ShowOverlay(OverlayA))
+
+ // Initial state.
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(/* empty */ )
+
+ // Swipe down to show overlay A.
+ val controller = onDragStarted(overSlop = down(0.1f))
+ val transition = assertThat(layoutState.transitionState).isShowOrHideOverlayTransition()
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasFromOrToScene(SceneA)
+ assertThat(transition).hasOverlay(OverlayA)
+ assertThat(transition).hasCurrentOverlays(/* empty, gesture not committed yet. */ )
+ assertThat(transition).hasProgress(0.1f)
+
+ // Commit the gesture. The overlay is instantly added in the set of current overlays.
+ controller.onDragStopped(velocityThreshold)
+ assertThat(transition).hasCurrentOverlays(OverlayA)
+ advanceUntilIdle()
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
+ }
+
+ @Test
+ fun hideOverlay() = runGestureTest {
+ layoutState.showOverlay(OverlayA, animationScope = testScope)
+ advanceUntilIdle()
+
+ // Initial state.
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
+
+ // Swipe up to hide overlay A.
+ val controller = onDragStarted(overSlop = up(0.1f))
+ val transition = assertThat(layoutState.transitionState).isShowOrHideOverlayTransition()
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasFromOrToScene(SceneA)
+ assertThat(transition).hasOverlay(OverlayA)
+ assertThat(transition).hasCurrentOverlays(OverlayA)
+ assertThat(transition).hasProgress(0.1f)
+
+ // Commit the gesture. The overlay is instantly removed from the set of current overlays.
+ controller.onDragStopped(-velocityThreshold)
+ assertThat(transition).hasCurrentOverlays(/* empty */ )
+ advanceUntilIdle()
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(/* empty */ )
+ }
+
+ @Test
+ fun replaceOverlay() = runGestureTest {
+ layoutState.showOverlay(OverlayA, animationScope = testScope)
+ advanceUntilIdle()
+
+ // Initial state.
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
+
+ // Swipe down to replace overlay A by overlay B.
+ val controller = onDragStarted(overSlop = down(0.1f))
+ val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasFromOverlay(OverlayA)
+ assertThat(transition).hasToOverlay(OverlayB)
+ assertThat(transition).hasCurrentOverlays(OverlayA)
+ assertThat(transition).hasProgress(0.1f)
+
+ // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
+ controller.onDragStopped(velocityThreshold)
+ assertThat(transition).hasCurrentOverlays(OverlayB)
+ advanceUntilIdle()
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
+ }
}
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 85db418f6020..bec2bb2baa3c 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
@@ -275,7 +275,7 @@ class OverlayTest {
rule.setContent {
SceneTransitionLayout(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
- overlay(OverlayA, alignment) { Foo() }
+ overlay(OverlayA, alignment = alignment) { Foo() }
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
index a98bd7652c4e..3fb57084a461 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene.subjects
import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.OverscrollSpec
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.content.state.TransitionState
@@ -33,7 +34,23 @@ fun assertThat(state: TransitionState): TransitionStateSubject {
/** Assert on a [TransitionState.Transition.ChangeCurrentScene]. */
fun assertThat(transition: TransitionState.Transition.ChangeCurrentScene): SceneTransitionSubject {
- return Truth.assertAbout(SceneTransitionSubject.transitions()).that(transition)
+ return Truth.assertAbout(SceneTransitionSubject.sceneTransitions()).that(transition)
+}
+
+/** Assert on a [TransitionState.Transition.ShowOrHideOverlay]. */
+fun assertThat(
+ transition: TransitionState.Transition.ShowOrHideOverlay,
+): ShowOrHideOverlayTransitionSubject {
+ return Truth.assertAbout(ShowOrHideOverlayTransitionSubject.showOrHideOverlayTransitions())
+ .that(transition)
+}
+
+/** Assert on a [TransitionState.Transition.ReplaceOverlay]. */
+fun assertThat(
+ transition: TransitionState.Transition.ReplaceOverlay,
+): ReplaceOverlayTransitionSubject {
+ return Truth.assertAbout(ReplaceOverlayTransitionSubject.replaceOverlayTransitions())
+ .that(transition)
}
class TransitionStateSubject
@@ -45,6 +62,10 @@ private constructor(
check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
}
+ fun hasCurrentOverlays(vararg overlays: OverlayKey) {
+ check("currentOverlays").that(actual.currentOverlays).containsExactlyElementsIn(overlays)
+ }
+
fun isIdle(): TransitionState.Idle {
if (actual !is TransitionState.Idle) {
failWithActual(simpleFact("expected to be TransitionState.Idle"))
@@ -63,6 +84,24 @@ private constructor(
return actual as TransitionState.Transition.ChangeCurrentScene
}
+ fun isShowOrHideOverlayTransition(): TransitionState.Transition.ShowOrHideOverlay {
+ if (actual !is TransitionState.Transition.ShowOrHideOverlay) {
+ failWithActual(
+ simpleFact("expected to be TransitionState.Transition.ShowOrHideOverlay")
+ )
+ }
+
+ return actual as TransitionState.Transition.ShowOrHideOverlay
+ }
+
+ fun isReplaceOverlayTransition(): TransitionState.Transition.ReplaceOverlay {
+ if (actual !is TransitionState.Transition.ReplaceOverlay) {
+ failWithActual(simpleFact("expected to be TransitionState.Transition.ReplaceOverlay"))
+ }
+
+ return actual as TransitionState.Transition.ReplaceOverlay
+ }
+
companion object {
fun transitionStates() = Factory { metadata, actual: TransitionState ->
TransitionStateSubject(metadata, actual)
@@ -70,21 +109,16 @@ private constructor(
}
}
-class SceneTransitionSubject
-private constructor(
+abstract class BaseTransitionSubject<T : TransitionState.Transition>(
metadata: FailureMetadata,
- private val actual: TransitionState.Transition.ChangeCurrentScene,
+ protected val actual: T,
) : Subject(metadata, actual) {
fun hasCurrentScene(sceneKey: SceneKey) {
check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
}
- fun hasFromScene(sceneKey: SceneKey) {
- check("fromScene").that(actual.fromScene).isEqualTo(sceneKey)
- }
-
- fun hasToScene(sceneKey: SceneKey) {
- check("toScene").that(actual.toScene).isEqualTo(sceneKey)
+ fun hasCurrentOverlays(vararg overlays: OverlayKey) {
+ check("currentOverlays").that(actual.currentOverlays).containsExactlyElementsIn(overlays)
}
fun hasProgress(progress: Float, tolerance: Float = 0f) {
@@ -144,11 +178,67 @@ private constructor(
.that((actual as TransitionState.HasOverscrollProperties).bouncingContent)
.isEqualTo(content)
}
+}
+
+class SceneTransitionSubject
+private constructor(
+ metadata: FailureMetadata,
+ actual: TransitionState.Transition.ChangeCurrentScene,
+) : BaseTransitionSubject<TransitionState.Transition.ChangeCurrentScene>(metadata, actual) {
+ fun hasFromScene(sceneKey: SceneKey) {
+ check("fromScene").that(actual.fromScene).isEqualTo(sceneKey)
+ }
+
+ fun hasToScene(sceneKey: SceneKey) {
+ check("toScene").that(actual.toScene).isEqualTo(sceneKey)
+ }
companion object {
- fun transitions() =
+ fun sceneTransitions() =
Factory { metadata, actual: TransitionState.Transition.ChangeCurrentScene ->
SceneTransitionSubject(metadata, actual)
}
}
}
+
+class ShowOrHideOverlayTransitionSubject
+private constructor(
+ metadata: FailureMetadata,
+ actual: TransitionState.Transition.ShowOrHideOverlay,
+) : BaseTransitionSubject<TransitionState.Transition.ShowOrHideOverlay>(metadata, actual) {
+ fun hasFromOrToScene(fromOrToScene: SceneKey) {
+ check("fromOrToScene").that(actual.fromOrToScene).isEqualTo(fromOrToScene)
+ }
+
+ fun hasOverlay(overlay: OverlayKey) {
+ check("overlay").that(actual.overlay).isEqualTo(overlay)
+ }
+
+ companion object {
+ fun showOrHideOverlayTransitions() =
+ Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay ->
+ ShowOrHideOverlayTransitionSubject(metadata, actual)
+ }
+ }
+}
+
+class ReplaceOverlayTransitionSubject
+private constructor(
+ metadata: FailureMetadata,
+ actual: TransitionState.Transition.ReplaceOverlay,
+) : BaseTransitionSubject<TransitionState.Transition.ReplaceOverlay>(metadata, actual) {
+ fun hasFromOverlay(fromOverlay: OverlayKey) {
+ check("fromOverlay").that(actual.fromOverlay).isEqualTo(fromOverlay)
+ }
+
+ fun hasToOverlay(toOverlay: OverlayKey) {
+ check("toOverlay").that(actual.toOverlay).isEqualTo(toOverlay)
+ }
+
+ companion object {
+ fun replaceOverlayTransitions() =
+ Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay ->
+ ReplaceOverlayTransitionSubject(metadata, actual)
+ }
+ }
+}
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 1ebd3d98471b..c5a5173cb037 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
@@ -200,8 +200,8 @@ fun ComposeContentTestRule.testReplaceOverlayTransition(
transitionLayout = { state ->
SceneTransitionLayout(state) {
scene(currentScene) { currentSceneContent() }
- overlay(from, fromAlignment) { fromContent() }
- overlay(to, toAlignment) { toContent() }
+ overlay(from, alignment = fromAlignment) { fromContent() }
+ overlay(to, alignment = toAlignment) { toContent() }
}
},
changeState = { state -> state.replaceOverlay(from, to, animationScope = this) },
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index b82b49caf737..b04ac1433f48 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -185,6 +185,9 @@ constructor(
)
}
}
+ is UserActionResult.ShowOverlay,
+ is UserActionResult.HideOverlay,
+ is UserActionResult.ReplaceByOverlay -> TODO("b/353679003: Support overlays")
} ?: actionResult
}
}