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

Commit c4a8ae2a authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Simplify the swipe offset animation logic

This CL simplifies the logic when animating the offset of a swipe
transition, after the user lifted their finger. The previous
implementation was made more complicated just to avoid allocating more
than one Animatable per SwipeTransition, which does not really have an
impact on performance anyways.

Bug: 290930950
Test: atest DraggableHandlerTest
Flag: N/A
Change-Id: I7b0d4bb1c6f29a3bfc02778e2fe8b03e80b5a6be
parent 0acf75e1
Loading
Loading
Loading
Loading
+31 −42
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package com.android.compose.animation.scene

import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.getValue
@@ -573,7 +574,7 @@ private class SwipeTransition(
            // Important: If we are going to return early because distance is equal to 0, we should
            // still make sure we read the offset before returning so that the calling code still
            // subscribes to the offset value.
            val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
            val offset = offsetAnimation?.animatable?.value ?: dragOffset

            val distance = distance()
            if (distance == DistanceUnspecified) {
@@ -588,20 +589,11 @@ private class SwipeTransition(
    /** 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)
    /** The offset animation that animates the offset once the user lifts their finger. */
    private var offsetAnimation: OffsetAnimation? by mutableStateOf(null)

    // 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)

    /** Job to check that there is at most one offset animation in progress. */
    private var offsetAnimationJob: Job? = null
        get() = offsetAnimation == null

    /**
     * The [TransformationSpecImpl] associated to this transition.
@@ -647,25 +639,21 @@ private class SwipeTransition(
        return distance
    }

    /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
    private fun startOffsetAnimation(job: () -> Job) {
    /** Ends any previous [offsetAnimation] and runs the new [animation]. */
    private fun startOffsetAnimation(animation: () -> OffsetAnimation) {
        cancelOffsetAnimation()
        offsetAnimationJob = job()
        offsetAnimation = animation()
    }

    /** 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()
    }
        val animation = offsetAnimation ?: return
        offsetAnimation = null

    fun finishOffsetAnimation() {
        if (isAnimatingOffset) {
            isAnimatingOffset = false
            dragOffset = offsetAnimatable.value
        }
        dragOffset = animation.animatable.value
        animation.job.cancel()
    }

    fun animateOffset(
@@ -676,29 +664,30 @@ private class SwipeTransition(
        onAnimationCompleted: () -> Unit,
    ) {
        startOffsetAnimation {
            val animatable = Animatable(dragOffset, OffsetVisibilityThreshold)
            val job =
                coroutineScope.launch {
                animateOffset(targetOffset, initialVelocity)
                    animatable.animateTo(
                        targetValue = targetOffset,
                        animationSpec = swipeSpec,
                        initialVelocity = initialVelocity,
                    )

                    onAnimationCompleted()
                }

            OffsetAnimation(animatable, job)
        }
    }

    private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
        if (!isAnimatingOffset) {
            offsetAnimatable.snapTo(dragOffset)
        }
        isAnimatingOffset = true
    private class OffsetAnimation(
        /** The animatable used to animate the offset. */
        val animatable: Animatable<Float, AnimationVector1D>,

        val animationSpec = transformationSpec
        offsetAnimatable.animateTo(
            targetValue = targetOffset,
            animationSpec = swipeSpec,
            initialVelocity = initialVelocity,
        /** The job in which [animatable] is animated. */
        val job: Job,
    )

        finishOffsetAnimation()
    }

    companion object {
        const val DistanceUnspecified = 0f
    }
+11 −0
Original line number Diff line number Diff line
@@ -951,4 +951,15 @@ class DraggableHandlerTest {
        assertThat(transition).isNotNull()
        assertThat(transition!!.progress).isEqualTo(-0.1f)
    }

    @Test
    fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
        // Swipe up from the middle to transition to scene B.
        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
        val dragController = onDragStarted(startedPosition = middle, overSlop = up(0.1f))
        assertTransition(fromScene = SceneA, toScene = SceneB, isUserInputOngoing = true)

        dragController.onDragStopped(velocity = 0f)
        assertTransition(isUserInputOngoing = false)
    }
}