summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt471
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt8
2 files changed, 256 insertions, 223 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index b3d2bc994c08..c8fbad4f4eef 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -46,7 +46,7 @@ internal class SceneGestureHandler(
val draggable: DraggableHandler = SceneDraggableHandler(this)
private var _swipeTransition: SwipeTransition? = null
- internal var swipeTransition: SwipeTransition
+ private var swipeTransition: SwipeTransition
get() = _swipeTransition ?: error("SwipeTransition needs to be initialized")
set(value) {
_swipeTransition = value
@@ -92,10 +92,6 @@ internal class SceneGestureHandler(
/** The [Swipes] associated to the current gesture. */
private var swipes: Swipes? = null
- /** The [UserActionResult] associated to up and down swipes. */
- private var upOrLeftResult: UserActionResult? = null
- private var downOrRightResult: UserActionResult? = null
-
/**
* Whether we should immediately intercept a gesture.
*
@@ -128,7 +124,7 @@ internal class SceneGestureHandler(
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
swipeTransition.cancelOffsetAnimation()
- updateSwipesResults(swipeTransition._fromScene)
+ swipes!!.updateSwipesResults(swipeTransition._fromScene)
return
}
@@ -144,16 +140,24 @@ internal class SceneGestureHandler(
}
val fromScene = layoutImpl.scene(transitionState.currentScene)
- updateSwipes(fromScene, startedPosition, pointersDown)
-
- val result =
- findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true)
- ?: return
- updateTransition(SwipeTransition(fromScene, result), force = true)
- }
+ val newSwipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ swipes = newSwipes
+ val result = newSwipes.findUserActionResult(fromScene, overSlop, true)
+
+ // As we were unable to locate a valid target scene, the initial SwipeTransition cannot be
+ // defined.
+ if (result == null) return
+
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = newSwipes,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
- private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
- this.swipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ updateTransition(newSwipeTransition, force = true)
}
private fun computeSwipes(
@@ -210,13 +214,6 @@ internal class SceneGestureHandler(
}
}
- private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float {
- val targetSize = this.targetSize
- return with(distance ?: DefaultSwipeDistance) {
- layoutImpl.density.absoluteDistance(targetSize, orientation)
- }
- }
-
internal fun onDrag(delta: Float) {
if (delta == 0f || !isDrivingTransition) return
swipeTransition.dragOffset += delta
@@ -226,15 +223,17 @@ internal class SceneGestureHandler(
val isNewFromScene = fromScene.key != swipeTransition.fromScene
val result =
- findUserActionResult(
- fromScene,
- swipeTransition.dragOffset,
- updateSwipesResults = isNewFromScene,
+ swipes!!.findUserActionResult(
+ fromScene = fromScene,
+ directionOffset = swipeTransition.dragOffset,
+ updateSwipesResults = isNewFromScene
)
- ?: run {
- onDragStopped(delta, true)
- return
- }
+
+ if (result == null) {
+ onDragStopped(velocity = delta, canChangeScene = true)
+ return
+ }
+
swipeTransition.dragOffset += acceleratedOffset
if (
@@ -242,25 +241,20 @@ internal class SceneGestureHandler(
result.toScene != swipeTransition.toScene ||
result.transitionKey != swipeTransition.key
) {
- updateTransition(
- SwipeTransition(fromScene, result).apply {
- this.dragOffset = swipeTransition.dragOffset
- }
- )
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = swipes!!,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+ .apply { dragOffset = swipeTransition.dragOffset }
+
+ updateTransition(newSwipeTransition)
}
}
- private fun updateSwipesResults(fromScene: Scene) {
- val (upOrLeftResult, downOrRightResult) =
- computeSwipesResults(
- fromScene,
- this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()")
- )
-
- this.upOrLeftResult = upOrLeftResult
- this.downOrRightResult = downOrRightResult
- }
-
private fun computeSwipesResults(
fromScene: Scene,
swipes: Swipes
@@ -295,74 +289,20 @@ internal class SceneGestureHandler(
// If the swipe was not committed, don't do anything.
if (swipeTransition._currentScene != toScene) {
- return Pair(fromScene, 0f)
+ return fromScene to 0f
}
// If the offset is past the distance then let's change fromScene so that the user can swipe
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
- return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) {
- Pair(toScene, absoluteDistance)
- } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) {
- Pair(toScene, -absoluteDistance)
- } else {
- Pair(fromScene, 0f)
- }
- }
-
- /**
- * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
- *
- * @param fromScene the scene 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 target scenes should be updated to the current values
- * held in the Scenes map. Usually we don't want to update them while doing a drag, because
- * this could change the target scene (jump cutting) to a different scene, when some system
- * state changed the targets the background. However, an update is needed any time we
- * calculate the targets for a new fromScene.
- * @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].
- */
- private fun findUserActionResult(
- fromScene: Scene,
- directionOffset: Float,
- updateSwipesResults: Boolean,
- ): UserActionResult? {
- if (updateSwipesResults) updateSwipesResults(fromScene)
-
- return when {
- upOrLeftResult == null && downOrRightResult == null -> null
- (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
- upOrLeftResult
- else -> downOrRightResult
- }
- }
-
- /**
- * A strict version of [findUserActionResult] that will return null when there is no Scene in
- * [directionOffset] direction
- */
- private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
- return when {
- directionOffset > 0f -> upOrLeftResult
- directionOffset < 0f -> downOrRightResult
- else -> null
- }
- }
-
- private fun computeAbsoluteDistance(
- fromScene: Scene,
- result: UserActionResult,
- ): Float {
- return if (result == upOrLeftResult) {
- -fromScene.getAbsoluteDistance(result.distance)
+ return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) {
+ toScene to absoluteDistance
+ } else if (
+ offset >= absoluteDistance && swipes!!.downOrRightResult?.toScene == toScene.key
+ ) {
+ toScene to -absoluteDistance
} else {
- check(result == downOrRightResult)
- fromScene.getAbsoluteDistance(result.distance)
+ fromScene to 0f
}
}
@@ -430,19 +370,24 @@ internal class SceneGestureHandler(
if (startFromIdlePosition) {
// If there is a target scene, we start the overscroll animation.
- val result =
- findUserActionResultStrict(velocity)
- ?: run {
- // We will not animate
- layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
- return
- }
+ val result = swipes!!.findUserActionResultStrict(velocity)
+ if (result == null) {
+ // We will not animate
+ layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
+ return
+ }
- updateTransition(
- SwipeTransition(fromScene, result).apply {
- _currentScene = swipeTransition._currentScene
- }
- )
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = swipes!!,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+ .apply { _currentScene = swipeTransition._currentScene }
+
+ updateTransition(newSwipeTransition)
animateTo(targetScene = fromScene, targetOffset = 0f)
} else {
// We were between two scenes: animate to the initial scene.
@@ -486,134 +431,220 @@ internal class SceneGestureHandler(
}
}
- private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
- return SwipeTransition(
- result.transitionKey,
- fromScene,
- layoutImpl.scene(result.toScene),
- computeAbsoluteDistance(fromScene, result),
- )
+ companion object {
+ private const val TAG = "SceneGestureHandler"
}
+}
- internal class SwipeTransition(
- val key: TransitionKey?,
- val _fromScene: Scene,
- val _toScene: Scene,
- /**
- * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
- * above or to the left of [toScene].
- */
- val distance: Float,
- ) : TransitionState.Transition(_fromScene.key, _toScene.key) {
- var _currentScene by mutableStateOf(_fromScene)
- override val currentScene: SceneKey
- get() = _currentScene.key
-
- override val progress: Float
- get() {
- val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
- return offset / distance
- }
+private fun SwipeTransition(
+ fromScene: Scene,
+ result: UserActionResult,
+ swipes: Swipes,
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+): SwipeTransition {
+ val upOrLeftResult = swipes.upOrLeftResult
+ val downOrRightResult = swipes.downOrRightResult
+ val userActionDistance = result.distance ?: DefaultSwipeDistance
+ val absoluteDistance =
+ with(userActionDistance) {
+ layoutImpl.density.absoluteDistance(fromScene.targetSize, orientation)
+ }
+
+ return SwipeTransition(
+ key = result.transitionKey,
+ _fromScene = fromScene,
+ _toScene = layoutImpl.scene(result.toScene),
+ distance =
+ when (result) {
+ upOrLeftResult -> -absoluteDistance
+ downOrRightResult -> absoluteDistance
+ else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
+ },
+ )
+}
+
+private class SwipeTransition(
+ val key: TransitionKey?,
+ val _fromScene: Scene,
+ val _toScene: Scene,
+ /**
+ * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
+ * or to the left of [toScene]
+ */
+ val distance: Float,
+) : TransitionState.Transition(_fromScene.key, _toScene.key) {
+ var _currentScene by mutableStateOf(_fromScene)
+ override val currentScene: SceneKey
+ get() = _currentScene.key
+
+ override val progress: Float
+ get() {
+ val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+ return offset / distance
+ }
- override val isInitiatedByUserInput = true
+ override val isInitiatedByUserInput = true
- /** The current offset caused by the drag gesture. */
- var dragOffset by mutableFloatStateOf(0f)
+ /** The current offset caused by the drag gesture. */
+ var dragOffset by mutableFloatStateOf(0f)
- /**
- * Whether the offset is animated (the user lifted their finger) or if it is driven by
- * gesture.
- */
- var isAnimatingOffset by mutableStateOf(false)
+ /**
+ * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
+ */
+ var isAnimatingOffset by mutableStateOf(false)
- // If we are not animating offset, it means the offset is being driven by the user's finger.
- override val isUserInputOngoing: Boolean
- get() = !isAnimatingOffset
+ // If we are not animating offset, it means the offset is being driven by the user's finger.
+ override val isUserInputOngoing: Boolean
+ get() = !isAnimatingOffset
- /** The animatable used to animate the offset once the user lifted its finger. */
- val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
+ /** The animatable used to animate the offset once the user lifted its finger. */
+ val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
- /** Job to check that there is at most one offset animation in progress. */
- private var offsetAnimationJob: Job? = null
+ /** Job to check that there is at most one offset animation in progress. */
+ private var offsetAnimationJob: Job? = null
- /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
- lateinit var swipeSpec: SpringSpec<Float>
+ /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
+ lateinit var swipeSpec: SpringSpec<Float>
- /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
- private fun startOffsetAnimation(job: () -> Job) {
- cancelOffsetAnimation()
- offsetAnimationJob = job()
- }
+ /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
+ private fun startOffsetAnimation(job: () -> Job) {
+ cancelOffsetAnimation()
+ offsetAnimationJob = job()
+ }
+
+ /** Cancel any ongoing offset animation. */
+ // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
+ // the same time.
+ fun cancelOffsetAnimation() {
+ offsetAnimationJob?.cancel()
+ finishOffsetAnimation()
+ }
- /** Cancel any ongoing offset animation. */
- // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
- // the same time.
- fun cancelOffsetAnimation() {
- offsetAnimationJob?.cancel()
- finishOffsetAnimation()
+ fun finishOffsetAnimation() {
+ if (isAnimatingOffset) {
+ isAnimatingOffset = false
+ dragOffset = offsetAnimatable.value
}
+ }
- fun finishOffsetAnimation() {
- if (isAnimatingOffset) {
- isAnimatingOffset = false
- dragOffset = offsetAnimatable.value
+ fun animateOffset(
+ // TODO(b/317063114) The CoroutineScope should be removed.
+ coroutineScope: CoroutineScope,
+ initialVelocity: Float,
+ targetOffset: Float,
+ onAnimationCompleted: () -> Unit,
+ ) {
+ startOffsetAnimation {
+ coroutineScope.launch {
+ animateOffset(targetOffset, initialVelocity)
+ onAnimationCompleted()
}
}
+ }
- fun animateOffset(
- // TODO(b/317063114) The CoroutineScope should be removed.
- coroutineScope: CoroutineScope,
- initialVelocity: Float,
- targetOffset: Float,
- onAnimationCompleted: () -> Unit,
- ) {
- startOffsetAnimation {
- coroutineScope.launch {
- animateOffset(targetOffset, initialVelocity)
- onAnimationCompleted()
- }
- }
+ private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
+ if (!isAnimatingOffset) {
+ offsetAnimatable.snapTo(dragOffset)
}
+ isAnimatingOffset = true
- private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
- if (!isAnimatingOffset) {
- offsetAnimatable.snapTo(dragOffset)
- }
- isAnimatingOffset = true
+ offsetAnimatable.animateTo(
+ targetValue = targetOffset,
+ animationSpec = swipeSpec,
+ initialVelocity = initialVelocity,
+ )
- offsetAnimatable.animateTo(
- targetValue = targetOffset,
- animationSpec = swipeSpec,
- initialVelocity = initialVelocity,
- )
+ finishOffsetAnimation()
+ }
+}
+
+private object DefaultSwipeDistance : UserActionDistance {
+ override fun Density.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return when (orientation) {
+ Orientation.Horizontal -> fromSceneSize.width
+ Orientation.Vertical -> fromSceneSize.height
+ }.toFloat()
+ }
+}
- finishOffsetAnimation()
+/** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
+private class Swipes(
+ val upOrLeft: Swipe?,
+ val downOrRight: Swipe?,
+ val upOrLeftNoSource: Swipe?,
+ val downOrRightNoSource: Swipe?,
+) {
+ /** The [UserActionResult] associated to up and down swipes. */
+ var upOrLeftResult: UserActionResult? = null
+ var downOrRightResult: UserActionResult? = null
+
+ fun computeSwipesResults(fromScene: Scene): Pair<UserActionResult?, UserActionResult?> {
+ val userActions = fromScene.userActions
+ fun result(swipe: Swipe?): UserActionResult? {
+ return userActions[swipe ?: return null]
}
+
+ val upOrLeftResult = result(upOrLeft) ?: result(upOrLeftNoSource)
+ val downOrRightResult = result(downOrRight) ?: result(downOrRightNoSource)
+ return upOrLeftResult to downOrRightResult
}
- companion object {
- private const val TAG = "SceneGestureHandler"
+ fun updateSwipesResults(fromScene: Scene) {
+ val (upOrLeftResult, downOrRightResult) = computeSwipesResults(fromScene)
+
+ this.upOrLeftResult = upOrLeftResult
+ this.downOrRightResult = downOrRightResult
}
- private object DefaultSwipeDistance : UserActionDistance {
- override fun Density.absoluteDistance(
- fromSceneSize: IntSize,
- orientation: Orientation,
- ): Float {
- return when (orientation) {
- Orientation.Horizontal -> fromSceneSize.width
- Orientation.Vertical -> fromSceneSize.height
- }.toFloat()
+ /**
+ * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
+ *
+ * @param fromScene the scene 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 target scenes should be updated to the current values
+ * held in the Scenes map. Usually we don't want to update them while doing a drag, because
+ * this could change the target scene (jump cutting) to a different scene, when some system
+ * state changed the targets the background. However, an update is needed any time we
+ * calculate the targets for a new fromScene.
+ * @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(
+ fromScene: Scene,
+ directionOffset: Float,
+ updateSwipesResults: Boolean,
+ ): UserActionResult? {
+ if (updateSwipesResults) {
+ updateSwipesResults(fromScene)
+ }
+
+ return when {
+ upOrLeftResult == null && downOrRightResult == null -> null
+ (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
+ upOrLeftResult
+ else -> downOrRightResult
}
}
- /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
- private class Swipes(
- val upOrLeft: Swipe?,
- val downOrRight: Swipe?,
- val upOrLeftNoSource: Swipe?,
- val downOrRightNoSource: Swipe?,
- )
+ /**
+ * A strict version of [findUserActionResult] that will return null when there is no Scene in
+ * [directionOffset] direction
+ */
+ fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
+ return when {
+ directionOffset > 0f -> upOrLeftResult
+ directionOffset < 0f -> downOrRightResult
+ else -> null
+ }
+ }
}
private class SceneDraggableHandler(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index dacbdb484d0c..c91d29880ffb 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -127,6 +127,9 @@ class SceneGestureHandlerTest {
val progress: Float
get() = (transitionState as Transition).progress
+ val isUserInputOngoing: Boolean
+ get() = (transitionState as Transition).isUserInputOngoing
+
fun advanceUntilIdle() {
testScope.testScheduler.advanceUntilIdle()
}
@@ -538,12 +541,11 @@ class SceneGestureHandlerTest {
onDragStopped(velocity = velocityThreshold)
assertTransition(currentScene = SceneC)
- assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
- assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue()
+ assertThat(isUserInputOngoing).isFalse()
// Start a new gesture while the offset is animating
onDragStartedImmediately()
- assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
+ assertThat(isUserInputOngoing).isTrue()
}
@Test