diff options
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 } } |