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

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

Improve overscroll performance

This CL improves the performance of STL when overscrolling: before this
CL, releasing the finger during an overscroll would make the last
animation frame (at progress 0% or 100%) slower because the
overscrollSpec would go from a non-null value to a null value, only for
that frame, placing more elements than during the transition.

This CL changes the check done to compute SwipeTransition.isBouncing()
so that overscrollSpec does not unnecessarily change values at the end
of the transition.

See b/345632645#comment5 for a before/after comparison.

Bug: 345632645
Test: DraggableHandlerTest
Flag: com.android.systemui.scene_container
Change-Id: I0b16b432a610f5613d448d00f085c723926929ee
parent 1e8d0901
Loading
Loading
Loading
Loading
+13 −3
Original line number Diff line number Diff line
@@ -692,6 +692,8 @@ private class SwipeTransition(
        return startOffsetAnimation {
            val animatable = Animatable(dragOffset, OffsetVisibilityThreshold)
            val isTargetGreater = targetOffset > animatable.value
            val startedWhenOvercrollingTargetScene =
                if (targetScene == fromScene) progress < 0f else progress > 1f
            val job =
                coroutineScope
                    // Important: We start atomically to make sure that we start the coroutine even
@@ -718,10 +720,19 @@ private class SwipeTransition(
                                if (bouncingScene == null) {
                                    val isBouncing =
                                        if (isTargetGreater) {
                                            if (startedWhenOvercrollingTargetScene) {
                                                value >= targetOffset
                                            } else {
                                                value > targetOffset
                                            }
                                        } else {
                                            if (startedWhenOvercrollingTargetScene) {
                                                value <= targetOffset
                                            } else {
                                                value < targetOffset
                                            }
                                        }

                                    if (isBouncing) {
                                        bouncingScene = targetScene

@@ -739,7 +750,6 @@ private class SwipeTransition(
                                }
                            }
                        } finally {
                            bouncingScene = null
                            snapToScene(targetScene)
                        }
                    }
+166 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.compose.animation.scene

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.material3.Text
import androidx.compose.ui.geometry.Offset
@@ -1025,4 +1027,168 @@ class DraggableHandlerTest {
        runCurrent()
        assertIdle(SceneB)
    }

    @Test
    fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
        // Make scene B overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)

        val dragController = onDragStarted(startedPosition = middle, overSlop = up(0.5f))
        val transition = assertThat(transitionState).isTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneB)
        assertThat(transition).hasProgress(0.5f)

        // Release to B.
        dragController.onDragStopped(velocity = -velocityThreshold)
        advanceUntilIdle()

        // We didn't overscroll at the end of the transition.
        assertIdle(SceneB)
        assertThat(transition).hasProgress(1f)
        assertThat(transition).hasNoOverscrollSpec()
    }

    @Test
    fun overscroll_releaseBetween0And100Percent_down() = runGestureTest {
        // Make scene C overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)

        val dragController = onDragStarted(startedPosition = middle, overSlop = down(0.5f))
        val transition = assertThat(transitionState).isTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneC)
        assertThat(transition).hasProgress(0.5f)

        // Release to C.
        dragController.onDragStopped(velocity = velocityThreshold)
        advanceUntilIdle()

        // We didn't overscroll at the end of the transition.
        assertIdle(SceneC)
        assertThat(transition).hasProgress(1f)
        assertThat(transition).hasNoOverscrollSpec()
    }

    @Test
    fun overscroll_releaseAt150Percent_up() = runGestureTest {
        // Make scene B overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)

        val dragController = onDragStarted(startedPosition = middle, overSlop = up(1.5f))
        val transition = assertThat(transitionState).isTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneB)
        assertThat(transition).hasProgress(1.5f)

        // Release to B.
        dragController.onDragStopped(velocity = 0f)
        advanceUntilIdle()

        // We kept the overscroll at 100% so that the placement logic didn't change at the end of
        // the animation.
        assertIdle(SceneB)
        assertThat(transition).hasProgress(1f)
        assertThat(transition).hasOverscrollSpec()
    }

    @Test
    fun overscroll_releaseAt150Percent_down() = runGestureTest {
        // Make scene C overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)

        val dragController = onDragStarted(startedPosition = middle, overSlop = down(1.5f))
        val transition = assertThat(transitionState).isTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneC)
        assertThat(transition).hasProgress(1.5f)

        // Release to C.
        dragController.onDragStopped(velocity = 0f)
        advanceUntilIdle()

        // We kept the overscroll at 100% so that the placement logic didn't change at the end of
        // the animation.
        assertIdle(SceneC)
        assertThat(transition).hasProgress(1f)
        assertThat(transition).hasOverscrollSpec()
    }

    @Test
    fun overscroll_releaseAtNegativePercent_up() = runGestureTest {
        // Make Scene A overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        mutableUserActionsA.clear()
        mutableUserActionsA[Swipe.Up] = UserActionResult(SceneB)

        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
        val dragController = onDragStarted(startedPosition = middle, overSlop = down(1f))
        val transition = assertThat(transitionState).isTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneB)
        assertThat(transition).hasProgress(-1f)

        // Release to A.
        dragController.onDragStopped(velocity = 0f)
        advanceUntilIdle()

        // We kept the overscroll at 100% so that the placement logic didn't change at the end of
        // the animation.
        assertIdle(SceneA)
        assertThat(transition).hasProgress(0f)
        assertThat(transition).hasOverscrollSpec()
    }

    @Test
    fun overscroll_releaseAtNegativePercent_down() = runGestureTest {
        // Make Scene A overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        mutableUserActionsA.clear()
        mutableUserActionsA[Swipe.Down] = UserActionResult(SceneC)

        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
        val dragController = onDragStarted(startedPosition = middle, overSlop = up(1f))
        val transition = assertThat(transitionState).isTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneC)
        assertThat(transition).hasProgress(-1f)

        // Release to A.
        dragController.onDragStopped(velocity = 0f)
        advanceUntilIdle()

        // We kept the overscroll at 100% so that the placement logic didn't change at the end of
        // the animation.
        assertIdle(SceneA)
        assertThat(transition).hasProgress(0f)
        assertThat(transition).hasOverscrollSpec()
    }
}