summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/Android.bp1
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt12
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt13
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt28
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt5
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt65
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt12
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt3
16 files changed, 155 insertions, 7 deletions
diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp
index 682c49cfd19c..090e9ccedda0 100644
--- a/packages/SystemUI/compose/scene/Android.bp
+++ b/packages/SystemUI/compose/scene/Android.bp
@@ -42,6 +42,7 @@ android_library {
"androidx.compose.material3_material3",
"PlatformComposeCore",
+ "mechanics",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt
index 28116cb435e4..7d41a266adf2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.mechanics.GestureContext
import kotlinx.coroutines.CoroutineScope
/** Trigger a one-off transition to show or hide an overlay. */
@@ -118,6 +119,7 @@ private class OneOffShowOrHideOverlayTransition(
override val isInitiatedByUserInput: Boolean = false
override val isUserInputOngoing: Boolean = false
+ override val gestureContext: GestureContext? = null
override suspend fun run() {
oneOffAnimation.run()
@@ -144,6 +146,7 @@ private class OneOffOverlayReplacingTransition(
override val isInitiatedByUserInput: Boolean = false
override val isUserInputOngoing: Boolean = false
+ override val gestureContext: GestureContext? = null
override suspend fun run() {
oneOffAnimation.run()
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 86be4a44f3cb..dad4e2491be0 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
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.mechanics.GestureContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -193,6 +194,7 @@ private class OneOffSceneTransition(
get() = oneOffAnimation.progressVelocity
override val isUserInputOngoing: Boolean = false
+ override val gestureContext: GestureContext? = null
override suspend fun run() {
oneOffAnimation.run()
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 633328a836e3..916d85a80e77 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -31,6 +31,8 @@ import com.android.compose.animation.scene.content.state.TransitionState.Compani
import com.android.compose.animation.scene.effect.GestureEffect
import com.android.compose.gesture.NestedDraggable
import com.android.compose.ui.util.SpaceVectorConverter
+import com.android.mechanics.DistanceGestureContext
+import com.android.mechanics.spec.InputDirection
import kotlin.math.absoluteValue
import kotlinx.coroutines.launch
@@ -114,7 +116,14 @@ internal class DraggableHandler(
else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
}
- return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation)
+ val gestureContext =
+ DistanceGestureContext(
+ initialDragOffset = 0f,
+ initialDirection = if (isUpOrLeft) InputDirection.Min else InputDirection.Max,
+ directionChangeSlop = layoutImpl.directionChangeSlop,
+ )
+
+ return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation, gestureContext)
}
private fun resolveSwipeSource(startedPosition: Offset): SwipeSource.Resolved? {
@@ -316,6 +325,7 @@ private class DragControllerImpl(
// when the distance is defined.
delta
}
+
distance > 0f -> desiredOffset.fastCoerceIn(0f, distance)
else -> desiredOffset.fastCoerceIn(distance, 0f)
}
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 8a6a0d6dbb99..621166e1823a 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
@@ -26,6 +26,8 @@ import com.android.compose.animation.scene.UserActionResult.HideOverlay
import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.compose.animation.scene.transition.animateProgress
+import com.android.mechanics.ProvidedGestureContext
+import com.android.mechanics.spec.InputDirection
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -55,6 +57,8 @@ internal fun PredictiveBackHandler(
// compute the distance. In our case the distance is always 1f.
orientation = Orientation.Horizontal,
distance = 1f,
+ gestureContext =
+ ProvidedGestureContext(dragOffset = 0f, direction = InputDirection.Max),
)
animateProgress(
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 4e389471d1d0..ef11932867a0 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
@@ -30,6 +30,7 @@ import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -716,6 +717,7 @@ internal fun SceneTransitionLayoutForTesting(
builder: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
+ val directionChangeSlop = LocalViewConfiguration.current.touchSlop
val layoutDirection = LocalLayoutDirection.current
val animationScope = rememberCoroutineScope()
val layoutImpl = remember {
@@ -731,6 +733,7 @@ internal fun SceneTransitionLayoutForTesting(
elements = sharedElementMap,
ancestors = ancestors,
lookaheadScope = lookaheadScope,
+ directionChangeSlop = directionChangeSlop,
)
.also { onLayoutImpl?.invoke(it) }
}
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 b4c449d0566c..38ad0a80fd00 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
@@ -88,6 +88,14 @@ internal class SceneTransitionLayoutImpl(
internal val animationScope: CoroutineScope,
/**
+ * Number of pixels a gesture has to travel in the opposite direction to for its intrinsic
+ * direction to change.
+ *
+ * Used to determine the direction of [Transition.gestureContext].
+ */
+ internal val directionChangeSlop: Float,
+
+ /**
* The map of [Element]s.
*
* Important: [Element]s from this map should never be accessed during composition because the
@@ -368,6 +376,7 @@ internal class SceneTransitionLayoutImpl(
error("Transition to the same scene is not supported. ${details()}")
}
}
+
is UserActionResult.ReplaceByOverlay -> {
check(key is OverlayKey) {
"ReplaceByOverlay() can only be used for overlays, not scenes. ${details()}"
@@ -377,6 +386,7 @@ internal class SceneTransitionLayoutImpl(
"Transition to the same overlay is not supported. ${details()}"
}
}
+
is UserActionResult.ShowOverlay,
is UserActionResult.HideOverlay -> {
/* Always valid. */
@@ -443,8 +453,10 @@ internal class SceneTransitionLayoutImpl(
maybeAdd(transition.toScene)
maybeAdd(transition.fromScene)
}
+
is TransitionState.Transition.ShowOrHideOverlay ->
maybeAdd(transition.fromOrToScene)
+
is TransitionState.Transition.ReplaceOverlay -> {}
}
}
@@ -510,6 +522,7 @@ internal class SceneTransitionLayoutImpl(
is TransitionState.Transition.ChangeScene -> {}
is TransitionState.Transition.ShowOrHideOverlay ->
maybeAdd(transition.overlay)
+
is TransitionState.Transition.ReplaceOverlay -> {
maybeAdd(transition.fromOverlay)
maybeAdd(transition.toOverlay)
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 b1d6d1efc264..4137f5f5725b 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
@@ -21,12 +21,13 @@ import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
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.util.fastCoerceIn
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.Companion.DistanceUnspecified
+import com.android.mechanics.GestureContext
+import com.android.mechanics.MutableDragOffsetGestureContext
import kotlin.math.absoluteValue
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.launch
@@ -37,6 +38,7 @@ internal fun createSwipeAnimation(
isUpOrLeft: Boolean,
orientation: Orientation,
distance: Float,
+ gestureContext: MutableDragOffsetGestureContext,
): SwipeAnimation<*> {
return createSwipeAnimation(
layoutState,
@@ -47,6 +49,7 @@ internal fun createSwipeAnimation(
contentForUserActions = {
error("Computing contentForUserActions requires a SceneTransitionLayoutImpl")
},
+ gestureContext = gestureContext,
)
}
@@ -55,6 +58,7 @@ internal fun createSwipeAnimation(
result: UserActionResult,
isUpOrLeft: Boolean,
orientation: Orientation,
+ gestureContext: MutableDragOffsetGestureContext,
distance: Float = DistanceUnspecified,
): SwipeAnimation<*> {
var lastDistance = distance
@@ -98,6 +102,7 @@ internal fun createSwipeAnimation(
orientation,
distance = ::distance,
contentForUserActions = { layoutImpl.contentForUserActions().key },
+ gestureContext = gestureContext,
)
}
@@ -108,6 +113,7 @@ private fun createSwipeAnimation(
orientation: Orientation,
distance: (SwipeAnimation<*>) -> Float,
contentForUserActions: () -> ContentKey,
+ gestureContext: MutableDragOffsetGestureContext,
): SwipeAnimation<*> {
fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> {
return SwipeAnimation(
@@ -118,6 +124,7 @@ private fun createSwipeAnimation(
isUpOrLeft = isUpOrLeft,
requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
distance = distance,
+ gestureContext = gestureContext,
)
}
@@ -132,6 +139,7 @@ private fun createSwipeAnimation(
)
.swipeAnimation
}
+
is UserActionResult.ShowOverlay -> {
val fromScene = layoutState.currentScene
val overlay = result.overlay
@@ -144,6 +152,7 @@ private fun createSwipeAnimation(
)
.swipeAnimation
}
+
is UserActionResult.HideOverlay -> {
val toScene = layoutState.currentScene
val overlay = result.overlay
@@ -156,11 +165,13 @@ private fun createSwipeAnimation(
)
.swipeAnimation
}
+
is UserActionResult.ReplaceByOverlay -> {
val fromOverlay =
when (val contentForUserActions = contentForUserActions()) {
is SceneKey ->
error("ReplaceByOverlay can only be called when an overlay is shown")
+
is OverlayKey -> contentForUserActions
}
@@ -186,8 +197,8 @@ internal class SwipeAnimation<T : ContentKey>(
val requiresFullDistanceSwipe: Boolean,
private val distance: (SwipeAnimation<T>) -> Float,
currentContent: T = fromContent,
- dragOffset: Float = 0f,
-) {
+ private val gestureContext: MutableDragOffsetGestureContext,
+) : MutableDragOffsetGestureContext by gestureContext {
/** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */
lateinit var contentTransition: TransitionState.Transition
@@ -254,9 +265,6 @@ internal class SwipeAnimation<T : ContentKey>(
val isInPreviewStage: Boolean
get() = contentTransition.previewTransformationSpec != null && currentContent == fromContent
- /** The current offset caused by the drag gesture. */
- var dragOffset by mutableFloatStateOf(dragOffset)
-
/** The offset animation that animates the offset once the user lifts their finger. */
private var offsetAnimation: Animatable<Float, AnimationVector1D>? by mutableStateOf(null)
private val offsetAnimationRunnable = CompletableDeferred<suspend () -> Unit>()
@@ -387,6 +395,7 @@ internal class SwipeAnimation<T : ContentKey>(
return when (val transition = contentTransition) {
is TransitionState.Transition.ChangeScene ->
layoutState.canChangeScene(targetContent as SceneKey)
+
is TransitionState.Transition.ShowOrHideOverlay -> {
if (targetContent == transition.overlay) {
layoutState.canShowOverlay(transition.overlay)
@@ -394,6 +403,7 @@ internal class SwipeAnimation<T : ContentKey>(
layoutState.canHideOverlay(transition.overlay)
}
}
+
is TransitionState.Transition.ReplaceOverlay -> {
val to = targetContent as OverlayKey
val from =
@@ -464,6 +474,8 @@ private class ChangeSceneSwipeTransition(
override val isUserInputOngoing: Boolean
get() = swipeAnimation.isUserInputOngoing
+ override val gestureContext: GestureContext = swipeAnimation
+
override suspend fun run() {
swipeAnimation.run()
}
@@ -515,6 +527,8 @@ private class ShowOrHideOverlaySwipeTransition(
override val isUserInputOngoing: Boolean
get() = swipeAnimation.isUserInputOngoing
+ override val gestureContext: GestureContext = swipeAnimation
+
override suspend fun run() {
swipeAnimation.run()
}
@@ -562,6 +576,8 @@ private class ReplaceOverlaySwipeTransition(
override val isUserInputOngoing: Boolean
get() = swipeAnimation.isUserInputOngoing
+ override val gestureContext: GestureContext = swipeAnimation
+
override suspend fun run() {
swipeAnimation.run()
}
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 f772f1a0b4a6..e9542c830b4d 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
@@ -33,6 +33,7 @@ import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransitionKey
import com.android.internal.jank.Cuj.CujType
+import com.android.mechanics.GestureContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -238,6 +239,9 @@ sealed interface TransitionState {
/** Whether user input is currently driving the transition. */
abstract val isUserInputOngoing: Boolean
+ /** Additional gesture context whenever the transition is driven by a user gesture. */
+ abstract val gestureContext: GestureContext?
+
/** The CUJ covered by this transition. */
@CujType
val cuj: Int?
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt
index c6912d5e4ad2..819cec712808 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt
@@ -29,6 +29,8 @@ import com.android.compose.animation.scene.SwipeAnimation
import com.android.compose.animation.scene.TransitionKey
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.createSwipeAnimation
+import com.android.mechanics.ProvidedGestureContext
+import com.android.mechanics.spec.InputDirection
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -126,6 +128,9 @@ private suspend fun MutableSceneTransitionLayoutState.seek(
// overscroll, which is disabled for progress-based transitions.
orientation = Orientation.Horizontal,
isUpOrLeft = false,
+ // There is no gesture information available here - animateProgress
+ // will set the progress as the dragOffset.
+ gestureContext = ProvidedGestureContext(0f, InputDirection.Max),
)
animateProgress(
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 4a0c330be4f8..ef360770bc41 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
@@ -41,7 +41,9 @@ import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.gesture.NestedDraggable
import com.android.compose.test.MonotonicClockTestScope
import com.android.compose.test.runMonotonicClockTest
+import com.android.mechanics.spec.InputDirection
import com.google.common.truth.Truth.assertThat
+import kotlin.math.nextUp
import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -106,6 +108,7 @@ class DraggableHandlerTest {
}
val transitionInterceptionThreshold = 0.05f
+ val directionChangeSlop = 10f
private val layoutImpl =
SceneTransitionLayoutImpl(
@@ -120,6 +123,7 @@ class DraggableHandlerTest {
// Use testScope and not backgroundScope here because backgroundScope does not
// work well with advanceUntilIdle(), which is used by some tests.
animationScope = testScope,
+ directionChangeSlop = directionChangeSlop,
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
@@ -871,4 +875,65 @@ class DraggableHandlerTest {
assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
}
+
+ @Test
+ fun gestureContext_dragOffset_matchesOverSlopAtBeginning() = runGestureTest {
+ val overSlop = down(fractionOfScreen = 0.1f)
+ onDragStarted(overSlop = overSlop)
+
+ val gestureContext = assertThat(transitionState).hasGestureContext()
+ assertThat(gestureContext.dragOffset).isEqualTo(overSlop)
+ }
+
+ @Test
+ fun gestureContext_dragOffset_getsUpdatedOnEachDragEvent() = runGestureTest {
+ val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
+
+ val gestureContext = assertThat(transitionState).hasGestureContext()
+ val initialDragOffset = gestureContext.dragOffset
+
+ dragController.onDragDelta(pixels = 3.5f)
+ assertThat(gestureContext.dragOffset).isEqualTo(initialDragOffset + 3.5f)
+
+ dragController.onDragDelta(pixels = -2f)
+ assertThat(gestureContext.dragOffset).isEqualTo(initialDragOffset + 3.5f - 2f)
+ }
+
+ @Test
+ fun gestureContext_direction_swipeDown_startsWithMaxDirection() = runGestureTest {
+ onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
+
+ val gestureContext = assertThat(transitionState).hasGestureContext()
+ assertThat(gestureContext.direction).isEqualTo(InputDirection.Max)
+ }
+
+ @Test
+ fun gestureContext_direction_swipeUp_startsWithMinDirection() = runGestureTest {
+ onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
+
+ val gestureContext = assertThat(transitionState).hasGestureContext()
+ assertThat(gestureContext.direction).isEqualTo(InputDirection.Min)
+ }
+
+ @Test
+ fun gestureContext_direction_withinDirectionSlop_staysSame() = runGestureTest {
+ val dragController = onDragStarted(overSlop = up(fractionOfScreen = .2f))
+
+ val gestureContext = assertThat(transitionState).hasGestureContext()
+ assertThat(gestureContext.direction).isEqualTo(InputDirection.Min)
+
+ dragController.onDragDelta(pixels = directionChangeSlop)
+ assertThat(gestureContext.direction).isEqualTo(InputDirection.Min)
+ }
+
+ @Test
+ fun gestureContext_direction_overDirectionSlop_isChanged() = runGestureTest {
+ val dragController = onDragStarted(overSlop = up(fractionOfScreen = .2f))
+
+ val gestureContext = assertThat(transitionState).hasGestureContext()
+ assertThat(gestureContext.direction).isEqualTo(InputDirection.Min)
+
+ dragController.onDragDelta(pixels = directionChangeSlop.nextUp())
+ assertThat(gestureContext.direction).isEqualTo(InputDirection.Max)
+ }
}
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 6db98a874787..8db7dbca2474 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
@@ -19,6 +19,7 @@ package com.android.compose.animation.scene.subjects
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.mechanics.GestureContext
import com.google.common.truth.Fact.simpleFact
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
@@ -98,6 +99,17 @@ private constructor(metadata: FailureMetadata, private val actual: TransitionSta
return actual as TransitionState.Transition.ReplaceOverlay
}
+ fun hasGestureContext(): GestureContext {
+ if (actual !is TransitionState.Transition) {
+ failWithActual(simpleFact("expected to be TransitionState.Transition"))
+ }
+
+ val gestureContext = ((actual as TransitionState.Transition).gestureContext)
+ check("transition.gestureContext").that(gestureContext).isNotNull()
+
+ return checkNotNull(gestureContext)
+ }
+
companion object {
fun transitionStates() = Factory { metadata, actual: TransitionState ->
TransitionStateSubject(metadata, actual)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt
index b9d01c2eaa3b..c22c19867dbb 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt
@@ -20,6 +20,7 @@ import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.content.state.TransitionState.Transition
+import com.android.mechanics.GestureContext
import kotlinx.coroutines.CompletableDeferred
/** A [Transition.ShowOrHideOverlay] for tests that will be finished once [finish] is called. */
@@ -84,6 +85,7 @@ fun transition(
override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
override val isUserInputOngoing: Boolean = isUserInputOngoing
+ override val gestureContext: GestureContext? = null
override fun freezeAndAnimateToCurrentState() {
if (onFreezeAndAnimate != null) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt
index 983c429aa58e..139dcd5d5114 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt
@@ -19,6 +19,7 @@ package com.android.compose.test
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.content.state.TransitionState.Transition
+import com.android.mechanics.GestureContext
import kotlinx.coroutines.CompletableDeferred
/** A [Transition.ShowOrHideOverlay] for tests that will be finished once [finish] is called. */
@@ -81,6 +82,7 @@ fun transition(
override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
override val isUserInputOngoing: Boolean = isUserInputOngoing
+ override val gestureContext: GestureContext? = null
override fun freezeAndAnimateToCurrentState() {
if (onFreezeAndAnimate != null) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt
index d11951ee4b24..18cd57bb985f 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt
@@ -19,6 +19,7 @@ package com.android.compose.test
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.content.state.TransitionState.Transition
+import com.android.mechanics.GestureContext
import kotlinx.coroutines.CompletableDeferred
/** A [Transition.ChangeScene] for tests that will be finished once [finish] is called. */
@@ -54,6 +55,7 @@ fun transition(
isUserInputOngoing: Boolean = false,
onFreezeAndAnimate: ((TestSceneTransition) -> Unit)? = null,
replacedTransition: Transition? = null,
+ gestureContext: GestureContext? = null,
): TestSceneTransition {
return object : TestSceneTransition(from, to, replacedTransition) {
override val currentScene: SceneKey
@@ -76,6 +78,7 @@ fun transition(
override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
override val isUserInputOngoing: Boolean = isUserInputOngoing
+ override val gestureContext: GestureContext? = gestureContext
override fun freezeAndAnimateToCurrentState() {
if (onFreezeAndAnimate != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 56fece8ecf5c..65fba28c5465 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -100,6 +100,7 @@ import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.compose.theme.PlatformTheme
+import com.android.mechanics.GestureContext
import com.android.systemui.Dumpable
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -935,6 +936,8 @@ private class ExpansionTransition(currentProgress: Float) :
override val isUserInputOngoing: Boolean
get() = true
+ override val gestureContext: GestureContext? = null
+
private val finishCompletable = CompletableDeferred<Unit>()
override suspend fun run() {