Merge changes from topic "stl-full-distance-swipe" into main
* changes:
Do not consider elements with alpha=0f when interrupting
Add UserActionResult.requiresFullDistanceSwipe
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 cfaed41..da968ac 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
@@ -395,10 +395,11 @@
if (
distance != DistanceUnspecified &&
shouldCommitSwipe(
- offset,
- distance,
- velocity,
+ offset = offset,
+ distance = distance,
+ velocity = velocity,
wasCommitted = swipeTransition._currentScene == toScene,
+ requiresFullDistanceSwipe = swipeTransition.requiresFullDistanceSwipe,
)
) {
targetScene = toScene
@@ -472,7 +473,12 @@
distance: Float,
velocity: Float,
wasCommitted: Boolean,
+ requiresFullDistanceSwipe: Boolean,
): Boolean {
+ if (requiresFullDistanceSwipe && !wasCommitted) {
+ return offset / distance >= 1f
+ }
+
fun isCloserToTarget(): Boolean {
return (offset - distance).absoluteValue < offset.absoluteValue
}
@@ -530,6 +536,7 @@
userActionDistanceScope = layoutImpl.userActionDistanceScope,
orientation = orientation,
isUpOrLeft = isUpOrLeft,
+ requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
)
}
@@ -545,6 +552,7 @@
orientation = old.orientation,
isUpOrLeft = old.isUpOrLeft,
lastDistance = old.lastDistance,
+ requiresFullDistanceSwipe = old.requiresFullDistanceSwipe,
)
.apply {
_currentScene = old._currentScene
@@ -562,6 +570,7 @@
val userActionDistanceScope: UserActionDistanceScope,
override val orientation: Orientation,
override val isUpOrLeft: Boolean,
+ val requiresFullDistanceSwipe: Boolean,
var lastDistance: Float = DistanceUnspecified,
) :
TransitionState.Transition(_fromScene.key, _toScene.key),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 09d11b7..d4f1ad1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -306,7 +306,6 @@
return layout(placeable.width, placeable.height) { place(transition, placeable) }
}
- @OptIn(ExperimentalComposeUiApi::class)
private fun Placeable.PlacementScope.place(
transition: TransitionState.Transition?,
placeable: Placeable,
@@ -561,10 +560,20 @@
}
private fun Element.SceneState.selfUpdateValuesBeforeInterruption() {
- offsetBeforeInterruption = lastOffset
sizeBeforeInterruption = lastSize
- scaleBeforeInterruption = lastScale
- alphaBeforeInterruption = lastAlpha
+
+ if (lastAlpha > 0f) {
+ offsetBeforeInterruption = lastOffset
+ scaleBeforeInterruption = lastScale
+ alphaBeforeInterruption = lastAlpha
+ } else {
+ // Consider the element as not placed in this scene if it was fully transparent.
+ // TODO(b/290930950): Look into using derived state inside place() instead to not even place
+ // the element at all when alpha == 0f.
+ offsetBeforeInterruption = Offset.Unspecified
+ scaleBeforeInterruption = Scale.Unspecified
+ alphaBeforeInterruption = Element.AlphaUnspecified
+ }
}
private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element.SceneState) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 33063c8..7c8fce8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -459,6 +459,13 @@
/** The key of the transition that should be used. */
val transitionKey: TransitionKey? = null,
+
+ /**
+ * If `true`, the swipe will be committed and we will settle to [toScene] if only if the user
+ * swiped at least the swipe distance, i.e. the transition progress was already equal to or
+ * bigger than 100% when the user released their finger. `
+ */
+ val requiresFullDistanceSwipe: Boolean = false,
)
fun interface UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index be3bca7..c738ad3 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1215,4 +1215,22 @@
onDragStartedImmediately()
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 50f / 75f)
}
+
+ @Test
+ fun requireFullDistanceSwipe() = runGestureTest {
+ mutableUserActionsA[Swipe.Up] = UserActionResult(SceneB, requiresFullDistanceSwipe = true)
+
+ val controller = onDragStarted(overSlop = up(fractionOfScreen = 0.9f))
+ assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.9f)
+
+ controller.onDragStopped(velocity = 0f)
+ advanceUntilIdle()
+ assertIdle(SceneA)
+
+ val otherController = onDragStarted(overSlop = up(fractionOfScreen = 1f))
+ assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
+ otherController.onDragStopped(velocity = 0f)
+ advanceUntilIdle()
+ assertIdle(SceneB)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 615a42f..7c20a97 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -851,11 +851,12 @@
rule.runOnUiThread {
MutableSceneTransitionLayoutState(
initialScene = SceneA,
- transitions = transitions {
- from(SceneA, to = SceneB) {
- translate(TestElements.Foo, y = translateY)
- }
- },
+ transitions =
+ transitions {
+ from(SceneA, to = SceneB) {
+ translate(TestElements.Foo, y = translateY)
+ }
+ },
)
as MutableSceneTransitionLayoutStateImpl
}
@@ -2010,4 +2011,81 @@
)
.isEqualTo(Element.SizeUnspecified)
}
+
+ @Test
+ fun transparentElementIsNotImpactingInterruption() = runTest {
+ val state =
+ rule.runOnIdle {
+ MutableSceneTransitionLayoutStateImpl(
+ SceneA,
+ transitions {
+ from(SceneA, to = SceneB) {
+ // In A => B, Foo is not shared and first fades out from A then fades in
+ // B.
+ sharedElement(TestElements.Foo, enabled = false)
+ fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) }
+ fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) }
+ }
+
+ from(SceneB, to = SceneA) {
+ // In B => A, Foo is shared.
+ sharedElement(TestElements.Foo, enabled = true)
+ }
+ }
+ )
+ }
+
+ @Composable
+ fun SceneScope.Foo(modifier: Modifier = Modifier) {
+ Box(modifier.element(TestElements.Foo).size(10.dp))
+ }
+
+ rule.setContent {
+ SceneTransitionLayout(state) {
+ scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
+
+ // Define A after B so that Foo is placed in A during A <=> B.
+ scene(SceneA) { Foo() }
+ }
+ }
+
+ // Start A => B at 70%.
+ rule.runOnUiThread {
+ state.startTransition(
+ transition(
+ from = SceneA,
+ to = SceneB,
+ progress = { 0.7f },
+ onFinish = neverFinish(),
+ )
+ )
+ }
+
+ rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(0.dp, 0.dp)
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(40.dp, 60.dp)
+
+ // Start B => A at 50% with interruptionProgress = 100%. Foo is placed in A and should still
+ // be at (40dp, 60dp) given that it was fully transparent in A before the interruption.
+ var interruptionProgress by mutableStateOf(1f)
+ rule.runOnUiThread {
+ state.startTransition(
+ transition(
+ from = SceneB,
+ to = SceneA,
+ progress = { 0.5f },
+ interruptionProgress = { interruptionProgress },
+ onFinish = neverFinish(),
+ )
+ )
+ }
+
+ rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(40.dp, 60.dp)
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()
+
+ // Set the interruption progress to 0%. Foo should be at (20dp, 30dp) given that B => is at
+ // 50%.
+ interruptionProgress = 0f
+ rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(20.dp, 30.dp)
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()
+ }
}