Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +34 −14 Original line number Diff line number Diff line Loading @@ -30,8 +30,7 @@ import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController import kotlin.math.absoluteValue internal typealias SuspendedValue<T> = suspend () -> T import kotlinx.coroutines.launch internal interface DraggableHandler { /** Loading @@ -50,6 +49,7 @@ internal interface DragController { /** * Drag the current scene by [delta] pixels. * * @param delta The distance to drag the scene in pixels. * @return the consumed [delta] */ fun onDrag(delta: Float): Float Loading @@ -57,9 +57,18 @@ internal interface DragController { /** * Stop the current drag with the given [velocity]. * * @param velocity The velocity of the drag when it stopped. * @param canChangeContent Whether the content can be changed as a result of this drag. * @return the consumed [velocity] when the animation complete */ fun onStop(velocity: Float, canChangeContent: Boolean): SuspendedValue<Float> suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float /** * Cancels the current drag. * * @param canChangeContent Whether the content can be changed as a result of this drag. */ fun onCancel(canChangeContent: Boolean) } internal class DraggableHandlerImpl( Loading Loading @@ -350,7 +359,7 @@ private class DragControllerImpl( val result = swipes.findUserActionResult(directionOffset = newOffset) if (result == null) { onStop(velocity = delta, canChangeContent = true) onCancel(canChangeContent = true) return 0f } Loading Loading @@ -379,11 +388,11 @@ private class DragControllerImpl( return consumedDelta } override fun onStop(velocity: Float, canChangeContent: Boolean): SuspendedValue<Float> { override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { return onStop(velocity, canChangeContent, swipeAnimation) } private fun <T : ContentKey> onStop( private suspend fun <T : ContentKey> onStop( velocity: Float, canChangeContent: Boolean, Loading @@ -392,14 +401,14 @@ private class DragControllerImpl( // callbacks (like onAnimationCompleted()) might incorrectly finish a new transition that // replaced this one. swipeAnimation: SwipeAnimation<T>, ): SuspendedValue<Float> { ): Float { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition || swipeAnimation.isAnimatingOffset()) { return { 0f } return 0f } val fromContent = swipeAnimation.fromContent val consumedVelocity: SuspendedValue<Float> val consumedVelocity: Float if (canChangeContent) { // If we are halfway between two contents, we check what the target will be based on the // velocity and offset of the transition, then we launch the animation. Loading Loading @@ -478,6 +487,12 @@ private class DragControllerImpl( isCloserToTarget() } } override fun onCancel(canChangeContent: Boolean) { swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f, canChangeContent = canChangeContent) } } } /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */ Loading Loading @@ -701,13 +716,14 @@ private fun scrollController( } override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { return dragController .onStop(velocity = initialVelocity, canChangeContent = canChangeScene) .invoke() return dragController.onStop( velocity = initialVelocity, canChangeContent = canChangeScene, ) } override fun onCancel() { dragController.onStop(velocity = 0f, canChangeContent = canChangeScene) dragController.onCancel(canChangeScene) } /** Loading @@ -731,5 +747,9 @@ internal const val OffsetVisibilityThreshold = 0.5f private object NoOpDragController : DragController { override fun onDrag(delta: Float) = 0f override fun onStop(velocity: Float, canChangeContent: Boolean) = suspend { 0f } override suspend fun onStop(velocity: Float, canChangeContent: Boolean) = 0f override fun onCancel(canChangeContent: Boolean) { /* do nothing */ } } packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +2 −6 Original line number Diff line number Diff line Loading @@ -318,17 +318,13 @@ internal class MultiPointerDraggableNode( velocityTracker.calculateVelocity(maxVelocity) } .toFloat(), onFling = { controller.onStop(it, canChangeContent = true).invoke() }, onFling = { controller.onStop(it, canChangeContent = true) }, ) }, onDragCancel = { controller -> startFlingGesture( initialVelocity = 0f, onFling = { controller.onStop(it, canChangeContent = true).invoke() }, onFling = { controller.onStop(it, canChangeContent = true) }, ) }, swipeDetector = swipeDetector, Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +8 −5 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ 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 import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch internal fun createSwipeAnimation( layoutState: MutableSceneTransitionLayoutStateImpl, Loading Loading @@ -317,11 +318,11 @@ internal class SwipeAnimation<T : ContentKey>( * * @return the velocity consumed */ fun animateOffset( suspend fun animateOffset( initialVelocity: Float, targetContent: T, spec: AnimationSpec<Float>? = null, ): SuspendedValue<Float> { ): Float { check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" } val initialProgress = progress Loading Loading @@ -379,7 +380,7 @@ internal class SwipeAnimation<T : ContentKey>( if (skipAnimation) { // Unblock the job. offsetAnimationRunnable.complete(null) return { 0f } return 0f } val isTargetGreater = targetOffset > animatable.value Loading Loading @@ -440,7 +441,7 @@ internal class SwipeAnimation<T : ContentKey>( } } return { velocityConsumed.await() } return velocityConsumed.await() } /** An exception thrown during the animation to stop it immediately. */ Loading Loading @@ -469,9 +470,11 @@ internal class SwipeAnimation<T : ContentKey>( fun freezeAndAnimateToCurrentState() { if (isAnimatingOffset()) return contentTransition.coroutineScope.launch { animateOffset(initialVelocity = 0f, targetContent = currentContent) } } } private object DefaultSwipeDistance : UserActionDistance { override fun UserActionDistanceScope.absoluteDistance( Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt +1 −1 Original line number Diff line number Diff line Loading @@ -145,7 +145,7 @@ internal suspend fun <T : ContentKey> animateProgress( cancelSpec: AnimationSpec<Float>?, animationScope: CoroutineScope? = null, ) { fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) { return } Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +13 −9 Original line number Diff line number Diff line Loading @@ -45,6 +45,8 @@ import com.android.compose.test.runMonotonicClockTest import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -266,7 +268,7 @@ class DraggableHandlerTest { ) { val velocityConsumed = onDragStoppedAnimateLater(velocity, canChangeScene) onAnimationStart() onAnimationEnd(velocityConsumed.invoke()) onAnimationEnd(velocityConsumed.await()) } suspend fun DragController.onDragStoppedAnimateNow( Loading @@ -285,8 +287,10 @@ class DraggableHandlerTest { fun DragController.onDragStoppedAnimateLater( velocity: Float, canChangeScene: Boolean = true, ): SuspendedValue<Float> { return onStop(velocity, canChangeScene) ): Deferred<Float> { val velocityConsumed = testScope.async { onStop(velocity, canChangeScene) } testScope.testScheduler.runCurrent() return velocityConsumed } fun NestedScrollConnection.scroll( Loading Loading @@ -1112,6 +1116,7 @@ class DraggableHandlerTest { // Freeze the transition. val transition = transitionState as Transition transition.freezeAndAnimateToCurrentState() runCurrent() assertTransition(isUserInputOngoing = false) advanceUntilIdle() assertIdle(SceneC) Loading Loading @@ -1279,14 +1284,13 @@ class DraggableHandlerTest { // Release the finger. dragController.onDragStoppedAnimateNow( velocity = -velocityThreshold, onAnimationStart = { assertTransition(fromScene = SceneA, toScene = SceneB) }, onAnimationStart = { // Given that we are at progress >= 100% and that the overscroll on scene B is doing // nothing, we are already idle. assertIdle(SceneB) }, expectedConsumedVelocity = 0f, ) // Exhaust all coroutines *without advancing the clock*. Given that we are at progress >= // 100% and that the overscroll on scene B is doing nothing, we are already idle. runCurrent() assertIdle(SceneB) } @Test Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +34 −14 Original line number Diff line number Diff line Loading @@ -30,8 +30,7 @@ import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController import kotlin.math.absoluteValue internal typealias SuspendedValue<T> = suspend () -> T import kotlinx.coroutines.launch internal interface DraggableHandler { /** Loading @@ -50,6 +49,7 @@ internal interface DragController { /** * Drag the current scene by [delta] pixels. * * @param delta The distance to drag the scene in pixels. * @return the consumed [delta] */ fun onDrag(delta: Float): Float Loading @@ -57,9 +57,18 @@ internal interface DragController { /** * Stop the current drag with the given [velocity]. * * @param velocity The velocity of the drag when it stopped. * @param canChangeContent Whether the content can be changed as a result of this drag. * @return the consumed [velocity] when the animation complete */ fun onStop(velocity: Float, canChangeContent: Boolean): SuspendedValue<Float> suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float /** * Cancels the current drag. * * @param canChangeContent Whether the content can be changed as a result of this drag. */ fun onCancel(canChangeContent: Boolean) } internal class DraggableHandlerImpl( Loading Loading @@ -350,7 +359,7 @@ private class DragControllerImpl( val result = swipes.findUserActionResult(directionOffset = newOffset) if (result == null) { onStop(velocity = delta, canChangeContent = true) onCancel(canChangeContent = true) return 0f } Loading Loading @@ -379,11 +388,11 @@ private class DragControllerImpl( return consumedDelta } override fun onStop(velocity: Float, canChangeContent: Boolean): SuspendedValue<Float> { override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { return onStop(velocity, canChangeContent, swipeAnimation) } private fun <T : ContentKey> onStop( private suspend fun <T : ContentKey> onStop( velocity: Float, canChangeContent: Boolean, Loading @@ -392,14 +401,14 @@ private class DragControllerImpl( // callbacks (like onAnimationCompleted()) might incorrectly finish a new transition that // replaced this one. swipeAnimation: SwipeAnimation<T>, ): SuspendedValue<Float> { ): Float { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition || swipeAnimation.isAnimatingOffset()) { return { 0f } return 0f } val fromContent = swipeAnimation.fromContent val consumedVelocity: SuspendedValue<Float> val consumedVelocity: Float if (canChangeContent) { // If we are halfway between two contents, we check what the target will be based on the // velocity and offset of the transition, then we launch the animation. Loading Loading @@ -478,6 +487,12 @@ private class DragControllerImpl( isCloserToTarget() } } override fun onCancel(canChangeContent: Boolean) { swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f, canChangeContent = canChangeContent) } } } /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */ Loading Loading @@ -701,13 +716,14 @@ private fun scrollController( } override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { return dragController .onStop(velocity = initialVelocity, canChangeContent = canChangeScene) .invoke() return dragController.onStop( velocity = initialVelocity, canChangeContent = canChangeScene, ) } override fun onCancel() { dragController.onStop(velocity = 0f, canChangeContent = canChangeScene) dragController.onCancel(canChangeScene) } /** Loading @@ -731,5 +747,9 @@ internal const val OffsetVisibilityThreshold = 0.5f private object NoOpDragController : DragController { override fun onDrag(delta: Float) = 0f override fun onStop(velocity: Float, canChangeContent: Boolean) = suspend { 0f } override suspend fun onStop(velocity: Float, canChangeContent: Boolean) = 0f override fun onCancel(canChangeContent: Boolean) { /* do nothing */ } }
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +2 −6 Original line number Diff line number Diff line Loading @@ -318,17 +318,13 @@ internal class MultiPointerDraggableNode( velocityTracker.calculateVelocity(maxVelocity) } .toFloat(), onFling = { controller.onStop(it, canChangeContent = true).invoke() }, onFling = { controller.onStop(it, canChangeContent = true) }, ) }, onDragCancel = { controller -> startFlingGesture( initialVelocity = 0f, onFling = { controller.onStop(it, canChangeContent = true).invoke() }, onFling = { controller.onStop(it, canChangeContent = true) }, ) }, swipeDetector = swipeDetector, Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +8 −5 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ 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 import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch internal fun createSwipeAnimation( layoutState: MutableSceneTransitionLayoutStateImpl, Loading Loading @@ -317,11 +318,11 @@ internal class SwipeAnimation<T : ContentKey>( * * @return the velocity consumed */ fun animateOffset( suspend fun animateOffset( initialVelocity: Float, targetContent: T, spec: AnimationSpec<Float>? = null, ): SuspendedValue<Float> { ): Float { check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" } val initialProgress = progress Loading Loading @@ -379,7 +380,7 @@ internal class SwipeAnimation<T : ContentKey>( if (skipAnimation) { // Unblock the job. offsetAnimationRunnable.complete(null) return { 0f } return 0f } val isTargetGreater = targetOffset > animatable.value Loading Loading @@ -440,7 +441,7 @@ internal class SwipeAnimation<T : ContentKey>( } } return { velocityConsumed.await() } return velocityConsumed.await() } /** An exception thrown during the animation to stop it immediately. */ Loading Loading @@ -469,9 +470,11 @@ internal class SwipeAnimation<T : ContentKey>( fun freezeAndAnimateToCurrentState() { if (isAnimatingOffset()) return contentTransition.coroutineScope.launch { animateOffset(initialVelocity = 0f, targetContent = currentContent) } } } private object DefaultSwipeDistance : UserActionDistance { override fun UserActionDistanceScope.absoluteDistance( Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt +1 −1 Original line number Diff line number Diff line Loading @@ -145,7 +145,7 @@ internal suspend fun <T : ContentKey> animateProgress( cancelSpec: AnimationSpec<Float>?, animationScope: CoroutineScope? = null, ) { fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) { return } Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +13 −9 Original line number Diff line number Diff line Loading @@ -45,6 +45,8 @@ import com.android.compose.test.runMonotonicClockTest import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -266,7 +268,7 @@ class DraggableHandlerTest { ) { val velocityConsumed = onDragStoppedAnimateLater(velocity, canChangeScene) onAnimationStart() onAnimationEnd(velocityConsumed.invoke()) onAnimationEnd(velocityConsumed.await()) } suspend fun DragController.onDragStoppedAnimateNow( Loading @@ -285,8 +287,10 @@ class DraggableHandlerTest { fun DragController.onDragStoppedAnimateLater( velocity: Float, canChangeScene: Boolean = true, ): SuspendedValue<Float> { return onStop(velocity, canChangeScene) ): Deferred<Float> { val velocityConsumed = testScope.async { onStop(velocity, canChangeScene) } testScope.testScheduler.runCurrent() return velocityConsumed } fun NestedScrollConnection.scroll( Loading Loading @@ -1112,6 +1116,7 @@ class DraggableHandlerTest { // Freeze the transition. val transition = transitionState as Transition transition.freezeAndAnimateToCurrentState() runCurrent() assertTransition(isUserInputOngoing = false) advanceUntilIdle() assertIdle(SceneC) Loading Loading @@ -1279,14 +1284,13 @@ class DraggableHandlerTest { // Release the finger. dragController.onDragStoppedAnimateNow( velocity = -velocityThreshold, onAnimationStart = { assertTransition(fromScene = SceneA, toScene = SceneB) }, onAnimationStart = { // Given that we are at progress >= 100% and that the overscroll on scene B is doing // nothing, we are already idle. assertIdle(SceneB) }, expectedConsumedVelocity = 0f, ) // Exhaust all coroutines *without advancing the clock*. Given that we are at progress >= // 100% and that the overscroll on scene B is doing nothing, we are already idle. runCurrent() assertIdle(SceneB) } @Test Loading