diff options
23 files changed, 401 insertions, 301 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index 192162475c9f..671b0128b621 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -98,7 +98,7 @@ private fun SceneScope.stateForQuickSettingsContent( else -> QSSceneAdapter.State.CLOSED } } - is TransitionState.Transition.ChangeCurrentScene -> + is TransitionState.Transition.ChangeScene -> with(transitionState) { when { isSplitShade -> UnsquishingQS(squishiness) 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 abe079a4ab64..e15bc1243dd9 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 @@ -28,7 +28,7 @@ internal fun CoroutineScope.animateToScene( layoutState: MutableSceneTransitionLayoutStateImpl, target: SceneKey, transitionKey: TransitionKey?, -): TransitionState.Transition.ChangeCurrentScene? { +): TransitionState.Transition.ChangeScene? { val transitionState = layoutState.transitionState if (transitionState.currentScene == target) { // This can happen in 3 different situations, for which there isn't anything else to do: @@ -55,7 +55,7 @@ internal fun CoroutineScope.animateToScene( replacedTransition = null, ) } - is TransitionState.Transition.ChangeCurrentScene -> { + is TransitionState.Transition.ChangeScene -> { val isInitiatedByUserInput = transitionState.isInitiatedByUserInput // A transition is currently running: first check whether `transition.toScene` or @@ -139,7 +139,7 @@ private fun CoroutineScope.animateToScene( reversed: Boolean = false, fromScene: SceneKey = layoutState.transitionState.currentScene, chain: Boolean = true, -): TransitionState.Transition.ChangeCurrentScene { +): TransitionState.Transition.ChangeScene { val oneOffAnimation = OneOffAnimation() val targetProgress = if (reversed) 0f else 1f val transition = @@ -184,7 +184,7 @@ private class OneOffSceneTransition( override val isInitiatedByUserInput: Boolean, replacedTransition: TransitionState.Transition?, private val oneOffAnimation: OneOffAnimation, -) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene, replacedTransition) { +) : TransitionState.Transition.ChangeScene(fromScene, toScene, replacedTransition) { override val progress: Float get() = oneOffAnimation.progress 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 71ff8a85159c..37e4daafdc7b 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 @@ -109,7 +109,7 @@ internal class DraggableHandlerImpl( // Only intercept the current transition if one of the 2 swipes results is also a transition // between the same pair of contents. val swipes = computeSwipes(startedPosition, pointersDown = 1) - val fromContent = swipeAnimation.currentContent + val fromContent = layoutImpl.content(swipeAnimation.currentContent) val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent) val currentScene = layoutImpl.state.currentScene val contentTransition = swipeAnimation.contentTransition @@ -145,7 +145,9 @@ internal class DraggableHandlerImpl( // We need to recompute the swipe results since this is a new gesture, and the // fromScene.userActions may have changed. val swipes = oldDragController.swipes - swipes.updateSwipesResults(fromContent = oldSwipeAnimation.fromContent) + swipes.updateSwipesResults( + fromContent = layoutImpl.content(oldSwipeAnimation.fromContent) + ) // A new gesture should always create a new SwipeAnimation. This way there cannot be // different gestures controlling the same transition. @@ -155,8 +157,10 @@ internal class DraggableHandlerImpl( val swipes = computeSwipes(startedPosition, pointersDown) val fromContent = layoutImpl.contentForUserActions() + + swipes.updateSwipesResults(fromContent) val result = - swipes.findUserActionResult(fromContent, overSlop, updateSwipesResults = true) + swipes.findUserActionResult(overSlop) // As we were unable to locate a valid target scene, the initial SwipeAnimation // cannot be defined. Consequently, a simple NoOp Controller will be returned. ?: return NoOpDragController @@ -188,7 +192,13 @@ internal class DraggableHandlerImpl( else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)") } - return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation) + return createSwipeAnimation( + layoutImpl, + layoutImpl.coroutineScope, + result, + isUpOrLeft, + orientation + ) } private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { @@ -291,7 +301,7 @@ private class DragControllerImpl( return onDrag(delta, swipeAnimation) } - private fun <T : Content> onDrag(delta: Float, swipeAnimation: SwipeAnimation<T>): Float { + private fun <T : ContentKey> onDrag(delta: Float, swipeAnimation: SwipeAnimation<T>): Float { if (delta == 0f || !isDrivingTransition || swipeAnimation.isFinishing) { return 0f } @@ -304,12 +314,12 @@ private class DragControllerImpl( fun hasReachedToSceneUpOrLeft() = distance < 0 && desiredOffset <= distance && - swipes.upOrLeftResult?.toContent(layoutState.currentScene) == toContent.key + swipes.upOrLeftResult?.toContent(layoutState.currentScene) == toContent fun hasReachedToSceneDownOrRight() = distance > 0 && desiredOffset >= distance && - swipes.downOrRightResult?.toContent(layoutState.currentScene) == toContent.key + swipes.downOrRightResult?.toContent(layoutState.currentScene) == toContent // Considering accelerated swipe: Change fromContent in the case where the user quickly // swiped multiple times in the same direction to accelerate the transition from A => B then @@ -321,7 +331,7 @@ private class DragControllerImpl( swipeAnimation.currentContent == toContent && (hasReachedToSceneUpOrLeft() || hasReachedToSceneDownOrRight()) - val fromContent: Content + val fromContent: ContentKey val currentTransitionOffset: Float val newOffset: Float val consumedDelta: Float @@ -357,12 +367,10 @@ private class DragControllerImpl( swipeAnimation.dragOffset = currentTransitionOffset - val result = - swipes.findUserActionResult( - fromContent = fromContent, - directionOffset = newOffset, - updateSwipesResults = hasReachedToContent - ) + if (hasReachedToContent) { + swipes.updateSwipesResults(draggableHandler.layoutImpl.content(fromContent)) + } + val result = swipes.findUserActionResult(directionOffset = newOffset) if (result == null) { onStop(velocity = delta, canChangeContent = true) @@ -371,7 +379,7 @@ private class DragControllerImpl( val needNewTransition = hasReachedToContent || - result.toContent(layoutState.currentScene) != swipeAnimation.toContent.key || + result.toContent(layoutState.currentScene) != swipeAnimation.toContent || result.transitionKey != swipeAnimation.contentTransition.key if (needNewTransition) { @@ -390,7 +398,7 @@ private class DragControllerImpl( return onStop(velocity, canChangeContent, swipeAnimation) } - private fun <T : Content> onStop( + private fun <T : ContentKey> onStop( velocity: Float, canChangeContent: Boolean, @@ -407,7 +415,6 @@ private class DragControllerImpl( fun animateTo(targetContent: T) { swipeAnimation.animateOffset( - coroutineScope = draggableHandler.coroutineScope, initialVelocity = velocity, targetContent = targetContent, ) @@ -518,6 +525,14 @@ internal class Swipes( return upOrLeftResult to downOrRightResult } + /** + * Update the swipes results. + * + * Usually we don't want to update them while doing a drag, because this could change the target + * content (jump cutting) to a different content, when some system state changed the targets the + * background. However, an update is needed any time we calculate the targets for a new + * fromContent. + */ fun updateSwipesResults(fromContent: Content) { val (upOrLeftResult, downOrRightResult) = computeSwipesResults(fromContent) @@ -526,31 +541,17 @@ internal class Swipes( } /** - * Returns the [UserActionResult] from [fromContent] in the direction of [directionOffset]. + * Returns the [UserActionResult] in the direction of [directionOffset]. * - * @param fromContent the content from which we look for the target * @param directionOffset signed float that indicates the direction. Positive is down or right * negative is up or left. - * @param updateSwipesResults whether the swipe results should be updated to the current values - * held in the user actions map. Usually we don't want to update them while doing a drag, - * because this could change the target content (jump cutting) to a different content, when - * some system state changed the targets the background. However, an update is needed any time - * we calculate the targets for a new fromContent. * @return null when there are no targets in either direction. If one direction is null and you * drag into the null direction this function will return the opposite direction, assuming * that the users intention is to start the drag into the other direction eventually. If * [directionOffset] is 0f and both direction are available, it will default to * [upOrLeftResult]. */ - fun findUserActionResult( - fromContent: Content, - directionOffset: Float, - updateSwipesResults: Boolean, - ): UserActionResult? { - if (updateSwipesResults) { - updateSwipesResults(fromContent) - } - + fun findUserActionResult(directionOffset: Float): UserActionResult? { return when { upOrLeftResult == null && downOrRightResult == null -> null (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt index 6181cfbb10eb..cb18c6729170 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt @@ -37,7 +37,7 @@ interface InterruptionHandler { * @see InterruptionResult */ fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey, ): InterruptionResult? } @@ -76,7 +76,7 @@ class InterruptionResult( */ object DefaultInterruptionHandler : InterruptionHandler { override fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey, ): InterruptionResult { return InterruptionResult( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt index a82ee4c359a3..3a7c2bf5d331 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt @@ -43,7 +43,7 @@ sealed interface ObservableTransitionState { fun currentScene(): Flow<SceneKey> { return when (this) { is Idle -> flowOf(currentScene) - is Transition.ChangeCurrentScene -> currentScene + is Transition.ChangeScene -> currentScene is Transition.ShowOrHideOverlay -> flowOf(currentScene) is Transition.ReplaceOverlay -> flowOf(currentScene) } @@ -94,7 +94,7 @@ sealed interface ObservableTransitionState { .trimMargin() /** A transition animating between [fromScene] and [toScene]. */ - class ChangeCurrentScene( + class ChangeScene( override val fromScene: SceneKey, override val toScene: SceneKey, val currentScene: Flow<SceneKey>, @@ -174,8 +174,8 @@ sealed interface ObservableTransitionState { previewProgress: Flow<Float> = flowOf(0f), isInPreviewStage: Flow<Boolean> = flowOf(false), currentOverlays: Flow<Set<OverlayKey>> = flowOf(emptySet()), - ): ChangeCurrentScene { - return ChangeCurrentScene( + ): ChangeScene { + return ChangeScene( fromScene, toScene, currentScene, @@ -210,8 +210,8 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans return snapshotFlow { when (val state = transitionState) { is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene) - is TransitionState.Transition.ChangeCurrentScene -> { - ObservableTransitionState.Transition.ChangeCurrentScene( + is TransitionState.Transition.ChangeScene -> { + ObservableTransitionState.Transition.ChangeScene( fromScene = state.fromScene, toScene = state.toScene, currentScene = snapshotFlow { state.currentScene }, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index e7e6b2a257d8..be4fea10602f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -18,120 +18,75 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.android.compose.animation.scene.content.state.TransitionState import kotlin.coroutines.cancellation.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch @Composable internal fun PredictiveBackHandler( - state: MutableSceneTransitionLayoutStateImpl, - coroutineScope: CoroutineScope, - targetSceneForBack: SceneKey? = null, + layoutImpl: SceneTransitionLayoutImpl, + result: UserActionResult?, ) { PredictiveBackHandler( - enabled = targetSceneForBack != null, + enabled = result != null, ) { progress: Flow<BackEventCompat> -> - val fromScene = state.transitionState.currentScene - if (targetSceneForBack == null || targetSceneForBack == fromScene) { + if (result == null) { // Note: We have to collect progress otherwise PredictiveBackHandler will throw. progress.first() return@PredictiveBackHandler } - val transition = - PredictiveBackTransition(state, coroutineScope, fromScene, toScene = targetSceneForBack) - state.startTransition(transition) - try { - progress.collect { backEvent -> transition.dragProgress = backEvent.progress } - - // Back gesture successful. - transition.animateTo(targetSceneForBack) - } catch (e: CancellationException) { - // Back gesture cancelled. - transition.animateTo(fromScene) - } + val animation = + createSwipeAnimation( + layoutImpl, + layoutImpl.coroutineScope, + result, + isUpOrLeft = false, + // Note that the orientation does not matter here given that it's only used to + // compute the distance. In our case the distance is always 1f. + orientation = Orientation.Horizontal, + distance = 1f, + ) + + animate(layoutImpl, animation, progress) } } -private class PredictiveBackTransition( - val state: MutableSceneTransitionLayoutStateImpl, - val coroutineScope: CoroutineScope, - fromScene: SceneKey, - toScene: SceneKey, -) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene) { - override var currentScene by mutableStateOf(fromScene) - private set - - /** The animated progress once the gesture was committed or cancelled. */ - private var progressAnimatable by mutableStateOf<Animatable<Float, AnimationVector1D>?>(null) - var dragProgress: Float by mutableFloatStateOf(0f) - - override val previewProgress: Float - get() = dragProgress - - override val previewProgressVelocity: Float - get() = 0f // Currently, velocity is not exposed by predictive back API - - override val isInPreviewStage: Boolean - get() = previewTransformationSpec != null && currentScene == fromScene - - override val progress: Float - get() = progressAnimatable?.value ?: previewTransformationSpec?.let { 0f } ?: dragProgress - - override val progressVelocity: Float - get() = progressAnimatable?.velocity ?: 0f - - override val isInitiatedByUserInput: Boolean - get() = true - - override val isUserInputOngoing: Boolean - get() = progressAnimatable == null - - private var animationJob: Job? = null - - override fun finish(): Job = animateTo(currentScene) - - fun animateTo(scene: SceneKey): Job { - check(scene == fromScene || scene == toScene) - animationJob?.let { - return it +private suspend fun <T : ContentKey> animate( + layoutImpl: SceneTransitionLayoutImpl, + animation: SwipeAnimation<T>, + progress: Flow<BackEventCompat>, +) { + fun animateOffset(targetContent: T) { + if ( + layoutImpl.state.transitionState != animation.contentTransition || animation.isFinishing + ) { + return } - if (scene != currentScene && state.transitionState == this && state.canChangeScene(scene)) { - currentScene = scene - } + animation.animateOffset( + initialVelocity = 0f, + targetContent = targetContent, + + // TODO(b/350705972): Allow to customize or reuse the same customization endpoints as + // the normal swipe transitions. We can't just reuse them here because other swipe + // transitions animate pixels while this transition animates progress, so the visibility + // thresholds will be completely different. + spec = spring(), + ) + } - val targetProgress = - when (currentScene) { - fromScene -> 0f - toScene -> 1f - else -> error("scene $currentScene should be either $fromScene or $toScene") - } - val startProgress = if (previewTransformationSpec != null) 0f else dragProgress - val animatable = Animatable(startProgress).also { progressAnimatable = it } + layoutImpl.state.startTransition(animation.contentTransition) + try { + progress.collect { backEvent -> animation.dragOffset = backEvent.progress } - // Important: We start atomically to make sure that we start the coroutine even if it is - // cancelled right after it is launched, so that finishTransition() is correctly called. - return coroutineScope - .launch(start = CoroutineStart.ATOMIC) { - try { - animatable.animateTo(targetProgress) - } finally { - state.finishTransition(this@PredictiveBackTransition) - } - } - .also { animationJob = it } + // Back gesture successful. + animateOffset(animation.toContent) + } catch (e: CancellationException) { + // Back gesture cancelled. + animateOffset(animation.fromContent) } } 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 258be8122c1d..b33b4f6c5019 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 @@ -353,19 +353,8 @@ internal class SceneTransitionLayoutImpl( @Composable private fun BackHandler() { - val targetSceneForBack = - 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) + val result = contentForUserActions().userActions[Back.Resolved] + PredictiveBackHandler(layoutImpl = this, result = result) } @Composable @@ -389,7 +378,7 @@ internal class SceneTransitionLayoutImpl( // Compose the new scene we are going to first. transitions.fastForEachReversed { transition -> when (transition) { - is TransitionState.Transition.ChangeCurrentScene -> { + is TransitionState.Transition.ChangeScene -> { maybeAdd(transition.toScene) maybeAdd(transition.fromScene) } @@ -439,7 +428,7 @@ internal class SceneTransitionLayoutImpl( transitions.fastForEach { transition -> when (transition) { - is TransitionState.Transition.ChangeCurrentScene -> {} + is TransitionState.Transition.ChangeScene -> {} is TransitionState.Transition.ShowOrHideOverlay -> maybeAdd(transition.overlay) is TransitionState.Transition.ReplaceOverlay -> { @@ -495,7 +484,7 @@ private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) : val width: Int val height: Int val transition = - layoutImpl.state.currentTransition as? TransitionState.Transition.ChangeCurrentScene + layoutImpl.state.currentTransition as? TransitionState.Transition.ChangeScene if (transition == null) { width = placeable.width height = placeable.height 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 47065c7581fc..f3128f1bf5c7 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 @@ -299,7 +299,7 @@ internal class MutableSceneTransitionLayoutStateImpl( targetScene: SceneKey, coroutineScope: CoroutineScope, transitionKey: TransitionKey?, - ): TransitionState.Transition.ChangeCurrentScene? { + ): TransitionState.Transition.ChangeScene? { checkThread() return coroutineScope.animateToScene( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 8ca90f18f3e0..57ff597d7314 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -18,15 +18,13 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.unit.IntSize -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 import kotlin.math.absoluteValue @@ -36,29 +34,96 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch internal fun createSwipeAnimation( + layoutState: MutableSceneTransitionLayoutStateImpl, + animationScope: CoroutineScope, + result: UserActionResult, + isUpOrLeft: Boolean, + orientation: Orientation, + distance: Float, +): SwipeAnimation<*> { + return createSwipeAnimation( + layoutState, + animationScope, + result, + isUpOrLeft, + orientation, + distance = { distance }, + contentForUserActions = { + error("Computing contentForUserActions requires a SceneTransitionLayoutImpl") + }, + ) +} + +internal fun createSwipeAnimation( layoutImpl: SceneTransitionLayoutImpl, + animationScope: CoroutineScope, + result: UserActionResult, + isUpOrLeft: Boolean, + orientation: Orientation, + distance: Float = DistanceUnspecified +): SwipeAnimation<*> { + var lastDistance = distance + + fun distance(animation: SwipeAnimation<*>): Float { + if (lastDistance != DistanceUnspecified) { + return lastDistance + } + + val absoluteDistance = + with(animation.contentTransition.transformationSpec.distance ?: DefaultSwipeDistance) { + layoutImpl.userActionDistanceScope.absoluteDistance( + layoutImpl.content(animation.fromContent).targetSize, + orientation, + ) + } + + if (absoluteDistance <= 0f) { + return DistanceUnspecified + } + + val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance + lastDistance = distance + return distance + } + + return createSwipeAnimation( + layoutImpl.state, + animationScope, + result, + isUpOrLeft, + orientation, + distance = ::distance, + contentForUserActions = { layoutImpl.contentForUserActions().key }, + ) +} + +private fun createSwipeAnimation( + layoutState: MutableSceneTransitionLayoutStateImpl, + animationScope: CoroutineScope, result: UserActionResult, isUpOrLeft: Boolean, orientation: Orientation, + distance: (SwipeAnimation<*>) -> Float, + contentForUserActions: () -> ContentKey, ): SwipeAnimation<*> { - fun <T : Content> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> { + fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> { return SwipeAnimation( - layoutImpl = layoutImpl, + layoutState = layoutState, + animationScope = animationScope, fromContent = fromContent, toContent = toContent, - userActionDistanceScope = layoutImpl.userActionDistanceScope, orientation = orientation, isUpOrLeft = isUpOrLeft, requiresFullDistanceSwipe = result.requiresFullDistanceSwipe, + distance = distance, ) } - val layoutState = layoutImpl.state return when (result) { is UserActionResult.ChangeScene -> { - val fromScene = layoutImpl.scene(layoutState.currentScene) - val toScene = layoutImpl.scene(result.toScene) - ChangeCurrentSceneSwipeTransition( + val fromScene = layoutState.currentScene + val toScene = result.toScene + ChangeSceneSwipeTransition( layoutState = layoutState, swipeAnimation = swipeAnimation(fromContent = fromScene, toContent = toScene), key = result.transitionKey, @@ -67,12 +132,12 @@ internal fun createSwipeAnimation( .swipeAnimation } is UserActionResult.ShowOverlay -> { - val fromScene = layoutImpl.scene(layoutState.currentScene) - val overlay = layoutImpl.overlay(result.overlay) + val fromScene = layoutState.currentScene + val overlay = result.overlay ShowOrHideOverlaySwipeTransition( layoutState = layoutState, - _fromOrToScene = fromScene, - _overlay = overlay, + fromOrToScene = fromScene, + overlay = overlay, swipeAnimation = swipeAnimation(fromContent = fromScene, toContent = overlay), key = result.transitionKey, replacedTransition = null, @@ -80,12 +145,12 @@ internal fun createSwipeAnimation( .swipeAnimation } is UserActionResult.HideOverlay -> { - val toScene = layoutImpl.scene(layoutState.currentScene) - val overlay = layoutImpl.overlay(result.overlay) + val toScene = layoutState.currentScene + val overlay = result.overlay ShowOrHideOverlaySwipeTransition( layoutState = layoutState, - _fromOrToScene = toScene, - _overlay = overlay, + fromOrToScene = toScene, + overlay = overlay, swipeAnimation = swipeAnimation(fromContent = overlay, toContent = toScene), key = result.transitionKey, replacedTransition = null, @@ -93,8 +158,14 @@ internal fun createSwipeAnimation( .swipeAnimation } is UserActionResult.ReplaceByOverlay -> { - val fromOverlay = layoutImpl.contentForUserActions() as Overlay - val toOverlay = layoutImpl.overlay(result.overlay) + val fromOverlay = + when (val contentForUserActions = contentForUserActions()) { + is SceneKey -> + error("ReplaceByOverlay can only be called when an overlay is shown") + is OverlayKey -> contentForUserActions + } + + val toOverlay = result.overlay ReplaceOverlaySwipeTransition( layoutState = layoutState, swipeAnimation = @@ -109,9 +180,8 @@ internal fun createSwipeAnimation( internal fun createSwipeAnimation(old: SwipeAnimation<*>): SwipeAnimation<*> { return when (val transition = old.contentTransition) { - is TransitionState.Transition.ChangeCurrentScene -> { - ChangeCurrentSceneSwipeTransition(transition as ChangeCurrentSceneSwipeTransition) - .swipeAnimation + is TransitionState.Transition.ChangeScene -> { + ChangeSceneSwipeTransition(transition as ChangeSceneSwipeTransition).swipeAnimation } is TransitionState.Transition.ShowOrHideOverlay -> { ShowOrHideOverlaySwipeTransition(transition as ShowOrHideOverlaySwipeTransition) @@ -125,15 +195,15 @@ internal fun createSwipeAnimation(old: SwipeAnimation<*>): SwipeAnimation<*> { } /** A helper class that contains the main logic for swipe transitions. */ -internal class SwipeAnimation<T : Content>( - val layoutImpl: SceneTransitionLayoutImpl, +internal class SwipeAnimation<T : ContentKey>( + val layoutState: MutableSceneTransitionLayoutStateImpl, + val animationScope: CoroutineScope, val fromContent: T, val toContent: T, - private val userActionDistanceScope: UserActionDistanceScope, override val orientation: Orientation, override val isUpOrLeft: Boolean, val requiresFullDistanceSwipe: Boolean, - private var lastDistance: Float = DistanceUnspecified, + private val distance: (SwipeAnimation<T>) -> Float, currentContent: T = fromContent, dragOffset: Float = 0f, ) : TransitionState.HasOverscrollProperties { @@ -147,7 +217,13 @@ internal class SwipeAnimation<T : Content>( // Important: If we are going to return early because distance is equal to 0, we should // still make sure we read the offset before returning so that the calling code still // subscribes to the offset value. - val offset = offsetAnimation?.animatable?.value ?: dragOffset + val animatable = offsetAnimation?.animatable + val offset = + when { + animatable != null -> animatable.value + contentTransition.previewTransformationSpec != null -> 0f + else -> dragOffset + } return computeProgress(offset) } @@ -172,6 +248,15 @@ internal class SwipeAnimation<T : Content>( return velocityInDistanceUnit / distance.absoluteValue } + val previewProgress: Float + get() = computeProgress(dragOffset) + + val previewProgressVelocity: Float + get() = 0f + + val isInPreviewStage: Boolean + get() = contentTransition.previewTransformationSpec != null && currentContent == fromContent + override var bouncingContent: ContentKey? = null /** The current offset caused by the drag gesture. */ @@ -183,17 +268,8 @@ internal class SwipeAnimation<T : Content>( val isUserInputOngoing: Boolean get() = offsetAnimation == null - override val overscrollScope: OverscrollScope = - object : OverscrollScope { - override val density: Float - get() = layoutImpl.density.density - - override val fontScale: Float - get() = layoutImpl.density.fontScale - - override val absoluteDistance: Float - get() = distance().absoluteValue - } + override val absoluteDistance: Float + get() = distance().absoluteValue /** Whether [finish] was called on this animation. */ var isFinishing = false @@ -202,14 +278,14 @@ internal class SwipeAnimation<T : Content>( constructor( other: SwipeAnimation<T> ) : this( - layoutImpl = other.layoutImpl, + layoutState = other.layoutState, + animationScope = other.animationScope, fromContent = other.fromContent, toContent = other.toContent, - userActionDistanceScope = other.userActionDistanceScope, orientation = other.orientation, isUpOrLeft = other.isUpOrLeft, requiresFullDistanceSwipe = other.requiresFullDistanceSwipe, - lastDistance = other.lastDistance, + distance = other.distance, currentContent = other.currentContent, dragOffset = other.dragOffset, ) @@ -222,27 +298,7 @@ internal class SwipeAnimation<T : Content>( * transition when the distance depends on the size or position of an element that is composed * in the content we are going to. */ - fun distance(): Float { - if (lastDistance != DistanceUnspecified) { - return lastDistance - } - - val absoluteDistance = - with(contentTransition.transformationSpec.distance ?: DefaultSwipeDistance) { - userActionDistanceScope.absoluteDistance( - fromContent.targetSize, - orientation, - ) - } - - if (absoluteDistance <= 0f) { - return DistanceUnspecified - } - - val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance - lastDistance = distance - return distance - } + fun distance(): Float = distance(this) /** Ends any previous [offsetAnimation] and runs the new [animation]. */ private fun startOffsetAnimation(animation: () -> OffsetAnimation): OffsetAnimation { @@ -262,10 +318,9 @@ internal class SwipeAnimation<T : Content>( } fun animateOffset( - // TODO(b/317063114) The CoroutineScope should be removed. - coroutineScope: CoroutineScope, initialVelocity: Float, targetContent: T, + spec: SpringSpec<Float>? = null, ): OffsetAnimation { val initialProgress = progress // Skip the animation if we have already reached the target content and the overscroll does @@ -304,12 +359,14 @@ internal class SwipeAnimation<T : Content>( } return startOffsetAnimation { - val animatable = Animatable(dragOffset, OffsetVisibilityThreshold) + val startProgress = + if (contentTransition.previewTransformationSpec != null) 0f else dragOffset + val animatable = Animatable(startProgress, OffsetVisibilityThreshold) val isTargetGreater = targetOffset > animatable.value val startedWhenOvercrollingTargetContent = if (targetContent == fromContent) initialProgress < 0f else initialProgress > 1f val job = - coroutineScope + animationScope // Important: We start atomically to make sure that we start the coroutine even // if it is cancelled right after it is launched, so that snapToContent() is // correctly called. Otherwise, this transition will never be stopped and we @@ -325,8 +382,9 @@ internal class SwipeAnimation<T : Content>( try { val swipeSpec = - contentTransition.transformationSpec.swipeSpec - ?: layoutImpl.state.transitions.defaultSwipeSpec + spec + ?: contentTransition.transformationSpec.swipeSpec + ?: layoutState.transitions.defaultSwipeSpec animatable.animateTo( targetValue = targetOffset, animationSpec = swipeSpec, @@ -349,7 +407,7 @@ internal class SwipeAnimation<T : Content>( } if (isBouncing) { - bouncingContent = targetContent.key + bouncingContent = targetContent // Immediately stop this transition if we are bouncing on a // content that does not bounce. @@ -368,20 +426,19 @@ internal class SwipeAnimation<T : Content>( } } - private fun canChangeContent(targetContent: Content): Boolean { - val layoutState = layoutImpl.state + private fun canChangeContent(targetContent: ContentKey): Boolean { return when (val transition = contentTransition) { - is TransitionState.Transition.ChangeCurrentScene -> - layoutState.canChangeScene(targetContent.key as SceneKey) + is TransitionState.Transition.ChangeScene -> + layoutState.canChangeScene(targetContent as SceneKey) is TransitionState.Transition.ShowOrHideOverlay -> { - if (targetContent.key == transition.overlay) { + if (targetContent == transition.overlay) { layoutState.canShowOverlay(transition.overlay) } else { layoutState.canHideOverlay(transition.overlay) } } is TransitionState.Transition.ReplaceOverlay -> { - val to = targetContent.key as OverlayKey + val to = targetContent as OverlayKey val from = if (to == transition.toOverlay) transition.fromOverlay else transition.toOverlay layoutState.canReplaceOverlay(from, to) @@ -392,7 +449,7 @@ internal class SwipeAnimation<T : Content>( private fun snapToContent(content: T) { cancelOffsetAnimation() check(currentContent == content) - layoutImpl.state.finishTransition(contentTransition) + layoutState.finishTransition(contentTransition) } fun finish(): Job { @@ -405,12 +462,7 @@ internal class SwipeAnimation<T : Content>( } // Animate to the current content. - val animation = - animateOffset( - coroutineScope = layoutImpl.coroutineScope, - initialVelocity = 0f, - targetContent = currentContent, - ) + val animation = animateOffset(initialVelocity = 0f, targetContent = currentContent) check(offsetAnimation == animation) return animation.job } @@ -436,21 +488,21 @@ private object DefaultSwipeDistance : UserActionDistance { } } -private class ChangeCurrentSceneSwipeTransition( +private class ChangeSceneSwipeTransition( val layoutState: MutableSceneTransitionLayoutStateImpl, - val swipeAnimation: SwipeAnimation<Scene>, + val swipeAnimation: SwipeAnimation<SceneKey>, override val key: TransitionKey?, - replacedTransition: ChangeCurrentSceneSwipeTransition?, + replacedTransition: ChangeSceneSwipeTransition?, ) : - TransitionState.Transition.ChangeCurrentScene( - swipeAnimation.fromContent.key, - swipeAnimation.toContent.key, + TransitionState.Transition.ChangeScene( + swipeAnimation.fromContent, + swipeAnimation.toContent, replacedTransition, ), TransitionState.HasOverscrollProperties by swipeAnimation { constructor( - other: ChangeCurrentSceneSwipeTransition + other: ChangeSceneSwipeTransition ) : this( layoutState = other.layoutState, swipeAnimation = SwipeAnimation(other.swipeAnimation), @@ -463,7 +515,7 @@ private class ChangeCurrentSceneSwipeTransition( } override val currentScene: SceneKey - get() = swipeAnimation.currentContent.key + get() = swipeAnimation.currentContent override val progress: Float get() = swipeAnimation.progress @@ -471,6 +523,15 @@ private class ChangeCurrentSceneSwipeTransition( override val progressVelocity: Float get() = swipeAnimation.progressVelocity + override val previewProgress: Float + get() = swipeAnimation.previewProgress + + override val previewProgressVelocity: Float + get() = swipeAnimation.previewProgressVelocity + + override val isInPreviewStage: Boolean + get() = swipeAnimation.isInPreviewStage + override val isInitiatedByUserInput: Boolean = true override val isUserInputOngoing: Boolean @@ -481,17 +542,17 @@ private class ChangeCurrentSceneSwipeTransition( private class ShowOrHideOverlaySwipeTransition( val layoutState: MutableSceneTransitionLayoutStateImpl, - val swipeAnimation: SwipeAnimation<Content>, - val _overlay: Overlay, - val _fromOrToScene: Scene, + val swipeAnimation: SwipeAnimation<ContentKey>, + overlay: OverlayKey, + fromOrToScene: SceneKey, override val key: TransitionKey?, replacedTransition: ShowOrHideOverlaySwipeTransition?, ) : TransitionState.Transition.ShowOrHideOverlay( - _overlay.key, - _fromOrToScene.key, - swipeAnimation.fromContent.key, - swipeAnimation.toContent.key, + overlay, + fromOrToScene, + swipeAnimation.fromContent, + swipeAnimation.toContent, replacedTransition, ), TransitionState.HasOverscrollProperties by swipeAnimation { @@ -500,8 +561,8 @@ private class ShowOrHideOverlaySwipeTransition( ) : this( layoutState = other.layoutState, swipeAnimation = SwipeAnimation(other.swipeAnimation), - _overlay = other._overlay, - _fromOrToScene = other._fromOrToScene, + overlay = other.overlay, + fromOrToScene = other.fromOrToScene, key = other.key, replacedTransition = other, ) @@ -511,7 +572,7 @@ private class ShowOrHideOverlaySwipeTransition( } override val isEffectivelyShown: Boolean - get() = swipeAnimation.currentContent == _overlay + get() = swipeAnimation.currentContent == overlay override val progress: Float get() = swipeAnimation.progress @@ -519,6 +580,15 @@ private class ShowOrHideOverlaySwipeTransition( override val progressVelocity: Float get() = swipeAnimation.progressVelocity + override val previewProgress: Float + get() = swipeAnimation.previewProgress + + override val previewProgressVelocity: Float + get() = swipeAnimation.previewProgressVelocity + + override val isInPreviewStage: Boolean + get() = swipeAnimation.isInPreviewStage + override val isInitiatedByUserInput: Boolean = true override val isUserInputOngoing: Boolean @@ -529,13 +599,13 @@ private class ShowOrHideOverlaySwipeTransition( private class ReplaceOverlaySwipeTransition( val layoutState: MutableSceneTransitionLayoutStateImpl, - val swipeAnimation: SwipeAnimation<Overlay>, + val swipeAnimation: SwipeAnimation<OverlayKey>, override val key: TransitionKey?, replacedTransition: ReplaceOverlaySwipeTransition?, ) : TransitionState.Transition.ReplaceOverlay( - swipeAnimation.fromContent.key, - swipeAnimation.toContent.key, + swipeAnimation.fromContent, + swipeAnimation.toContent, replacedTransition, ), TransitionState.HasOverscrollProperties by swipeAnimation { @@ -553,7 +623,7 @@ private class ReplaceOverlaySwipeTransition( } override val effectivelyShownOverlay: OverlayKey - get() = swipeAnimation.currentContent.key + get() = swipeAnimation.currentContent override val progress: Float get() = swipeAnimation.progress @@ -561,6 +631,15 @@ private class ReplaceOverlaySwipeTransition( override val progressVelocity: Float get() = swipeAnimation.progressVelocity + override val previewProgress: Float + get() = swipeAnimation.previewProgress + + override val previewProgressVelocity: Float + get() = swipeAnimation.previewProgressVelocity + + override val isInPreviewStage: Boolean + get() = swipeAnimation.isInPreviewStage + override val isInitiatedByUserInput: Boolean = true override val isUserInputOngoing: Boolean diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index fdb019f5a604..0cd8c1af0507 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.getValue import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.OverlayKey -import com.android.compose.animation.scene.OverscrollScope import com.android.compose.animation.scene.OverscrollSpecImpl import com.android.compose.animation.scene.ProgressVisibilityThreshold import com.android.compose.animation.scene.SceneKey @@ -75,7 +74,7 @@ sealed interface TransitionState { val replacedTransition: Transition? = null, ) : TransitionState { /** A transition animating between [fromScene] and [toScene]. */ - abstract class ChangeCurrentScene( + abstract class ChangeScene( /** The scene this transition is starting from. Can't be the same as toScene */ val fromScene: SceneKey, @@ -386,10 +385,10 @@ sealed interface TransitionState { val orientation: Orientation /** - * Scope which can be used in the Overscroll DSL to define a transformation based on the - * distance between [Transition.fromContent] and [Transition.toContent]. + * Return the absolute distance between fromScene and toScene, if available, otherwise + * [DistanceUnspecified]. */ - val overscrollScope: OverscrollScope + val absoluteDistance: Float /** * The content (scene or overlay) around which the transition is currently bouncing. When diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 59bca50f7d5b..8f845866a0f3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentKey @@ -53,6 +54,8 @@ internal class OverscrollTranslate( val x: OverscrollScope.() -> Float = { 0f }, val y: OverscrollScope.() -> Float = { 0f }, ) : PropertyTransformation<Offset> { + private val cachedOverscrollScope = CachedOverscrollScope() + override fun transform( layoutImpl: SceneTransitionLayoutImpl, content: ContentKey, @@ -65,10 +68,47 @@ internal class OverscrollTranslate( // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume // that this method was invoked after performing this check. val overscrollProperties = transition as TransitionState.HasOverscrollProperties + val overscrollScope = + cachedOverscrollScope.getFromCacheOrCompute(layoutImpl.density, overscrollProperties) return Offset( - x = value.x + overscrollProperties.overscrollScope.x(), - y = value.y + overscrollProperties.overscrollScope.y(), + x = value.x + overscrollScope.x(), + y = value.y + overscrollScope.y(), ) } } + +/** + * A helper class to cache a [OverscrollScope] given a [Density] and + * [TransitionState.HasOverscrollProperties]. This helps avoid recreating a scope every frame + * whenever an overscroll transition is computed. + */ +private class CachedOverscrollScope() { + private var previousScope: OverscrollScope? = null + private var previousDensity: Density? = null + private var previousOverscrollProperties: TransitionState.HasOverscrollProperties? = null + + fun getFromCacheOrCompute( + density: Density, + overscrollProperties: TransitionState.HasOverscrollProperties, + ): OverscrollScope { + if ( + previousScope == null || + density != previousDensity || + previousOverscrollProperties != overscrollProperties + ) { + val scope = + object : OverscrollScope, Density by density { + override val absoluteDistance: Float + get() = overscrollProperties.absoluteDistance + } + + previousScope = scope + previousDensity = density + previousOverscrollProperties = overscrollProperties + return scope + } + + return checkNotNull(previousScope) + } +} 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 59ddb1354073..564d4b3a3c5a 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 @@ -27,7 +27,7 @@ internal class LinkedTransition( fromScene: SceneKey, toScene: SceneKey, override val key: TransitionKey? = null, -) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene) { +) : TransitionState.Transition.ChangeScene(fromScene, toScene) { override val currentScene: SceneKey get() { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt index f4e60a2a4100..3f6bd2c38792 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt @@ -69,7 +69,7 @@ class InterruptionHandlerTest { interruptionHandler = object : InterruptionHandler { override fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey ): InterruptionResult { return InterruptionResult( @@ -104,7 +104,7 @@ class InterruptionHandlerTest { interruptionHandler = object : InterruptionHandler { override fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey ): InterruptionResult { return InterruptionResult( @@ -198,7 +198,7 @@ class InterruptionHandlerTest { companion object { val FromToCurrentTriple = Correspondence.transforming( - { transition: TransitionState.Transition.ChangeCurrentScene? -> + { transition: TransitionState.Transition.ChangeScene? -> Triple(transition?.fromScene, transition?.toScene, transition?.currentScene) }, "(from, to, current) triple" diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index a549d0355a26..e4879d9d8a31 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -163,7 +163,7 @@ class MovableElementTest { fromContentZIndex: Float, toContentZIndex: Float ): ContentKey { - transition as TransitionState.Transition.ChangeCurrentScene + transition as TransitionState.Transition.ChangeScene assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) assertThat(fromContentZIndex).isEqualTo(0) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt index 00c75882a587..c5b6cdf12385 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt @@ -20,10 +20,16 @@ import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +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 @@ -198,6 +204,42 @@ class PredictiveBackHandlerTest { assertThat(canChangeSceneCalled).isFalse() } + @Test + fun backDismissesOverlayWithHighestZIndexByDefault() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + initialOverlays = setOf(OverlayA, OverlayB) + ) + } + + rule.setContent { + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + overlay(OverlayA) { Box(Modifier.fillMaxSize()) } + overlay(OverlayB) { Box(Modifier.fillMaxSize()) } + } + } + + // Initial state. + rule.onNode(hasTestTag(SceneA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayB.testTag)).assertIsDisplayed() + + // Press back. This should hide overlay B because it has a higher zIndex than overlay A. + rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() } + rule.onNode(hasTestTag(SceneA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayB.testTag)).assertDoesNotExist() + + // Press back again. This should hide overlay A. + rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() } + rule.onNode(hasTestTag(SceneA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayA.testTag)).assertDoesNotExist() + rule.onNode(hasTestTag(OverlayB.testTag)).assertDoesNotExist() + } + private fun backEvent(progress: Float = 0f): BackEventCompat { return BackEventCompat( touchX = 0f, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt index 1f7fe3766971..467031afb262 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt @@ -42,9 +42,9 @@ fun transition( orientation: Orientation = Orientation.Horizontal, onFinish: ((TransitionState.Transition) -> Job)? = null, replacedTransition: TransitionState.Transition? = null, -): TransitionState.Transition.ChangeCurrentScene { +): TransitionState.Transition.ChangeScene { return object : - TransitionState.Transition.ChangeCurrentScene(from, to, replacedTransition), + TransitionState.Transition.ChangeScene(from, to, replacedTransition), TransitionState.HasOverscrollProperties { override val currentScene: SceneKey get() = current() @@ -69,12 +69,7 @@ fun transition( override val isUpOrLeft: Boolean = isUpOrLeft override val bouncingContent: ContentKey? = bouncingContent override val orientation: Orientation = orientation - override val overscrollScope: OverscrollScope = - object : OverscrollScope { - override val density: Float = 1f - override val fontScale: Float = 1f - override val absoluteDistance = 0f - } + override val absoluteDistance = 0f override fun finish(): Job { val onFinish = 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 3fb57084a461..44e0ba51f713 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 @@ -32,8 +32,8 @@ fun assertThat(state: TransitionState): TransitionStateSubject { return Truth.assertAbout(TransitionStateSubject.transitionStates()).that(state) } -/** Assert on a [TransitionState.Transition.ChangeCurrentScene]. */ -fun assertThat(transition: TransitionState.Transition.ChangeCurrentScene): SceneTransitionSubject { +/** Assert on a [TransitionState.Transition.ChangeScene]. */ +fun assertThat(transition: TransitionState.Transition.ChangeScene): SceneTransitionSubject { return Truth.assertAbout(SceneTransitionSubject.sceneTransitions()).that(transition) } @@ -74,14 +74,14 @@ private constructor( return actual as TransitionState.Idle } - fun isSceneTransition(): TransitionState.Transition.ChangeCurrentScene { - if (actual !is TransitionState.Transition.ChangeCurrentScene) { + fun isSceneTransition(): TransitionState.Transition.ChangeScene { + if (actual !is TransitionState.Transition.ChangeScene) { failWithActual( simpleFact("expected to be TransitionState.Transition.ChangeCurrentScene") ) } - return actual as TransitionState.Transition.ChangeCurrentScene + return actual as TransitionState.Transition.ChangeScene } fun isShowOrHideOverlayTransition(): TransitionState.Transition.ShowOrHideOverlay { @@ -183,8 +183,8 @@ abstract class BaseTransitionSubject<T : TransitionState.Transition>( class SceneTransitionSubject private constructor( metadata: FailureMetadata, - actual: TransitionState.Transition.ChangeCurrentScene, -) : BaseTransitionSubject<TransitionState.Transition.ChangeCurrentScene>(metadata, actual) { + actual: TransitionState.Transition.ChangeScene, +) : BaseTransitionSubject<TransitionState.Transition.ChangeScene>(metadata, actual) { fun hasFromScene(sceneKey: SceneKey) { check("fromScene").that(actual.fromScene).isEqualTo(sceneKey) } @@ -195,7 +195,7 @@ private constructor( companion object { fun sceneTransitions() = - Factory { metadata, actual: TransitionState.Transition.ChangeCurrentScene -> + Factory { metadata, actual: TransitionState.Transition.ChangeScene -> SceneTransitionSubject(metadata, actual) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 4d3909c06efc..f365afbfcc06 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -501,7 +501,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun getCurrentSceneInUi(): SceneKey { return when (val state = transitionState.value) { is ObservableTransitionState.Idle -> state.currentScene - is ObservableTransitionState.Transition.ChangeCurrentScene -> state.fromScene + is ObservableTransitionState.Transition.ChangeScene -> state.fromScene is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene is ObservableTransitionState.Transition.ReplaceOverlay -> state.currentScene } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt index ea61bd32c1f2..04620d6982d2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -118,7 +118,7 @@ constructor( get() = when (this) { is ObservableTransitionState.Idle -> currentScene.canBeOccluded - is ObservableTransitionState.Transition.ChangeCurrentScene -> + is ObservableTransitionState.Transition.ChangeScene -> fromScene.canBeOccluded && toScene.canBeOccluded is ObservableTransitionState.Transition.ReplaceOverlay, is ObservableTransitionState.Transition.ShowOrHideOverlay -> diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index bdb148acbb37..a2142b6ce30c 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -131,7 +131,7 @@ constructor( .map { state -> when (state) { is ObservableTransitionState.Idle -> null - is ObservableTransitionState.Transition.ChangeCurrentScene -> state.toScene + is ObservableTransitionState.Transition.ChangeScene -> state.toScene is ObservableTransitionState.Transition.ShowOrHideOverlay, is ObservableTransitionState.Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions") diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt index ec743ba5c91e..d1629c799732 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt @@ -112,7 +112,7 @@ constructor( // It // happens only when unlocking or when dismissing a dismissible lockscreen. val isTransitioningAwayFromKeyguard = - transitionState is ObservableTransitionState.Transition.ChangeCurrentScene && + transitionState is ObservableTransitionState.Transition.ChangeScene && transitionState.fromScene.isKeyguard() && transitionState.toScene == Scenes.Gone @@ -120,7 +120,7 @@ constructor( val isCurrentSceneShade = currentScene.isShade() // This is true when moving into one of the shade scenes when a non-shade scene. val isTransitioningToShade = - transitionState is ObservableTransitionState.Transition.ChangeCurrentScene && + transitionState is ObservableTransitionState.Transition.ChangeScene && !transitionState.fromScene.isShade() && transitionState.toScene.isShade() diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt index 7d6712166a21..e276f8807df7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt @@ -64,7 +64,7 @@ constructor( 0f } ) - is ObservableTransitionState.Transition.ChangeCurrentScene -> + is ObservableTransitionState.Transition.ChangeScene -> when { state.fromScene == Scenes.Gone -> if (state.toScene.isExpandable()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index b7f663314c5d..3e42413932f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ObservableTransitionState.Idle import com.android.compose.animation.scene.ObservableTransitionState.Transition -import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeCurrentScene +import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene import com.android.compose.animation.scene.SceneKey import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -77,7 +77,7 @@ constructor( } } - private fun fullyExpandedDuringSceneChange(change: ChangeCurrentScene): Boolean { + private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean { // The lockscreen stack is visible during all transitions away from the lockscreen, so keep // the stack expanded until those transitions finish. return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) || @@ -85,7 +85,7 @@ constructor( } private fun expandFractionDuringSceneChange( - change: ChangeCurrentScene, + change: ChangeScene, shadeExpansion: Float, qsExpansion: Float, ): Float { @@ -118,7 +118,7 @@ constructor( ) { shadeExpansion, _, qsExpansion, transitionState, _ -> when (transitionState) { is Idle -> if (expandedInScene(transitionState.currentScene)) 1f else 0f - is ChangeCurrentScene -> + is ChangeScene -> expandFractionDuringSceneChange( transitionState, shadeExpansion, @@ -248,7 +248,7 @@ constructor( } } -private fun ChangeCurrentScene.isBetween( +private fun ChangeScene.isBetween( a: (SceneKey) -> Boolean, - b: (SceneKey) -> Boolean + b: (SceneKey) -> Boolean, ): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene)) |