Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 78388235 authored by omarmt's avatar omarmt
Browse files

animateOffset returns the consumed velocity after the animation ends

This change enables us to wait for the animation to finish and allows us
to obtain the velocity computed at the end of the animation, even if
it's interrupted.

Test: atest DraggableHandlerTest
Test: atest PriorityNestedScrollConnectionTest
Bug: 336710600
Flag: com.android.systemui.scene_container
Change-Id: I91163d53f06b4622369db29ff7f9b2468ca6ac57
parent 19861d84
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -81,7 +81,7 @@ fun NotificationScrimNestedScrollConnection(
            if (scrimOffset() < minScrimOffset()) {
                animateScrimOffset(minScrimOffset())
            }
            0f
            { 0f }
        },
    )
}
+1 −1
Original line number Diff line number Diff line
@@ -100,7 +100,7 @@ fun NotificationStackNestedScrollConnection(
        },
        onStop = { velocityAvailable ->
            onStop(velocityAvailable)
            velocityAvailable
            suspend { velocityAvailable }
        },
    )
}
+8 −7
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import com.android.compose.nestedscroll.SuspendedValue
import kotlin.math.absoluteValue

internal interface DraggableHandler {
@@ -54,9 +55,9 @@ internal interface DragController {
    /**
     * Stop the current drag with the given [velocity].
     *
     * @return the consumed [velocity]
     * @return the consumed [velocity] when the animation complete
     */
    fun onStop(velocity: Float, canChangeContent: Boolean): Float
    fun onStop(velocity: Float, canChangeContent: Boolean): SuspendedValue<Float>
}

internal class DraggableHandlerImpl(
@@ -377,7 +378,7 @@ private class DragControllerImpl(
        return consumedDelta
    }

    override fun onStop(velocity: Float, canChangeContent: Boolean): Float {
    override fun onStop(velocity: Float, canChangeContent: Boolean): SuspendedValue<Float> {
        return onStop(velocity, canChangeContent, swipeAnimation)
    }

@@ -390,14 +391,14 @@ private class DragControllerImpl(
        // callbacks (like onAnimationCompleted()) might incorrectly finish a new transition that
        // replaced this one.
        swipeAnimation: SwipeAnimation<T>,
    ): Float {
    ): SuspendedValue<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: Float
        val consumedVelocity: SuspendedValue<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.
@@ -729,5 +730,5 @@ internal const val OffsetVisibilityThreshold = 0.5f
private object NoOpDragController : DragController {
    override fun onDrag(delta: Float) = 0f

    override fun onStop(velocity: Float, canChangeContent: Boolean) = 0f
    override fun onStop(velocity: Float, canChangeContent: Boolean) = suspend { 0f }
}
+11 −4
Original line number Diff line number Diff line
@@ -325,13 +325,17 @@ internal class MultiPointerDraggableNode(
                                            velocityTracker.calculateVelocity(maxVelocity)
                                        }
                                        .toFloat(),
                                onFling = { controller.onStop(it, canChangeContent = true) },
                                onFling = {
                                    controller.onStop(it, canChangeContent = true).invoke()
                                },
                            )
                        },
                        onDragCancel = { controller ->
                            startFlingGesture(
                                initialVelocity = 0f,
                                onFling = { controller.onStop(it, canChangeContent = true) },
                                onFling = {
                                    controller.onStop(it, canChangeContent = true).invoke()
                                },
                            )
                        },
                        swipeDetector = swipeDetector,
@@ -353,7 +357,10 @@ internal class MultiPointerDraggableNode(
     *
     * Note: Inspired by [androidx.compose.foundation.gestures.ScrollableNode.onDragStopped]
     */
    private fun startFlingGesture(initialVelocity: Float, onFling: (velocity: Float) -> Float) {
    private fun startFlingGesture(
        initialVelocity: Float,
        onFling: suspend (velocity: Float) -> Float,
    ) {
        // Note: [AwaitPointerEventScope] is annotated as @RestrictsSuspension, we need another
        // CoroutineScope to run the fling gestures.
        // We do not need to cancel this [Job], the source will take care of emitting an
@@ -415,7 +422,7 @@ internal class MultiPointerDraggableNode(
     */
    private suspend inline fun dispatchFlingEvents(
        availableOnPreFling: Float,
        onFling: (velocity: Float) -> Float,
        onFling: suspend (velocity: Float) -> Float,
    ): Float {
        // PreFling phase
        val consumedByPreFling =
+13 −6
Original line number Diff line number Diff line
@@ -27,8 +27,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.nestedscroll.SuspendedValue
import kotlinx.coroutines.CancellationException
import kotlin.math.absoluteValue
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeout

internal fun createSwipeAnimation(
    layoutState: MutableSceneTransitionLayoutStateImpl,
@@ -321,7 +324,7 @@ internal class SwipeAnimation<T : ContentKey>(
        initialVelocity: Float,
        targetContent: T,
        spec: AnimationSpec<Float>? = null,
    ): Float {
    ): SuspendedValue<Float> {
        check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" }

        val initialProgress = progress
@@ -360,7 +363,7 @@ internal class SwipeAnimation<T : ContentKey>(
            currentContent = targetContent
        }

        val startProgress =
        val initialOffset =
            if (contentTransition.previewTransformationSpec != null && targetContent == toContent) {
                0f
            } else {
@@ -368,7 +371,7 @@ internal class SwipeAnimation<T : ContentKey>(
            }

        val animatable =
            Animatable(startProgress, OffsetVisibilityThreshold).also { offsetAnimation = it }
            Animatable(initialOffset, OffsetVisibilityThreshold).also { offsetAnimation = it }

        check(isAnimatingOffset())

@@ -379,7 +382,7 @@ internal class SwipeAnimation<T : ContentKey>(
        if (skipAnimation) {
            // Unblock the job.
            offsetAnimationRunnable.complete(null)
            return 0f
            return { 0f }
        }

        val isTargetGreater = targetOffset > animatable.value
@@ -391,6 +394,8 @@ internal class SwipeAnimation<T : ContentKey>(
                ?: contentTransition.transformationSpec.swipeSpec
                ?: layoutState.transitions.defaultSwipeSpec

        val velocityConsumed = CompletableDeferred<Float>()

        offsetAnimationRunnable.complete {
            try {
                animatable.animateTo(
@@ -427,11 +432,13 @@ internal class SwipeAnimation<T : ContentKey>(
                }
            } catch (_: SnapException) {
                /* Ignore. */
            } finally {
                // The animation consumed the whole available velocity
                velocityConsumed.complete(initialVelocity)
            }
        }

        // This animation always consumes the whole available velocity
        return initialVelocity
        return { velocityConsumed.await() }
    }

    /** An exception thrown during the animation to stop it immediately. */
Loading