summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt8
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt67
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt12
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt141
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt21
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt289
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt9
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt44
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt6
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt42
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt11
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt12
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))