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

Commit e73b0cb0 authored by omarmt's avatar omarmt
Browse files

SwipeToScene must have onPreScroll priority during a scene transition

To support this case, PriorityNestedScrollConnection can now take
precedence even during preScroll, with its own initial conditions.

Test: atest PriorityNestedScrollConnectionTest
Bug: 291025415
Change-Id: I3ff22580d80b35ac6b94db64354422737ba783d7
parent 2c02d345
Loading
Loading
Loading
Loading
+20 −6
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.android.compose.nestedscroll.PriorityPostNestedScrollConnection
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -535,7 +535,7 @@ private class SceneDraggableHandler(
class SceneNestedScrollHandler(
    private val gestureHandler: SceneGestureHandler,
) : NestedScrollHandler {
    override val connection: PriorityPostNestedScrollConnection = nestedScrollConnection()
    override val connection: PriorityNestedScrollConnection = nestedScrollConnection()

    private fun Offset.toAmount() =
        when (gestureHandler.orientation) {
@@ -555,7 +555,7 @@ class SceneNestedScrollHandler(
            Orientation.Vertical -> Offset(x = 0f, y = this)
        }

    private fun nestedScrollConnection(): PriorityPostNestedScrollConnection {
    private fun nestedScrollConnection(): PriorityNestedScrollConnection {
        // The next potential scene is calculated during the canStart
        var nextScene: SceneKey? = null

@@ -566,10 +566,24 @@ class SceneNestedScrollHandler(
        // moving on to the next scene.
        var gestureStartedOnNestedChild = false

        return PriorityPostNestedScrollConnection(
            canStart = { offsetAvailable, offsetBeforeStart ->
        return PriorityNestedScrollConnection(
            canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
                gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero

                val canInterceptPreScroll =
                    gestureHandler.isDrivingTransition &&
                        !gestureStartedOnNestedChild &&
                        offsetAvailable.toAmount() != 0f

                if (!canInterceptPreScroll) return@PriorityNestedScrollConnection false

                nextScene = gestureHandler.swipeTransitionToScene.key

                true
            },
            canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
                val amount = offsetAvailable.toAmount()
                if (amount == 0f) return@PriorityPostNestedScrollConnection false
                if (amount == 0f) return@PriorityNestedScrollConnection false

                gestureStartedOnNestedChild = offsetBeforeStart != Offset.Zero

+32 −16
Original line number Diff line number Diff line
@@ -22,17 +22,18 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity

/**
 * This [NestedScrollConnection] waits for a child to scroll ([onPostScroll]), and then decides (via
 * [canStart]) if it should take over scrolling. If it does, it will scroll before its children,
 * until [canContinueScroll] allows it.
 * This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and
 * then decides (via [canStartPreScroll] or [canStartPostScroll]) if it should take over scrolling.
 * If it does, it will scroll before its children, until [canContinueScroll] allows it.
 *
 * Note: Call [reset] before destroying this object to make sure you always get a call to [onStop]
 * after [onStart].
 *
 * @sample com.android.compose.animation.scene.rememberSwipeToSceneNestedScrollConnection
 */
class PriorityPostNestedScrollConnection(
    private val canStart: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
class PriorityNestedScrollConnection(
    private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
    private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
    private val canContinueScroll: () -> Boolean,
    private val onStart: () -> Unit,
    private val onScroll: (offsetAvailable: Offset) -> Offset,
@@ -57,26 +58,21 @@ class PriorityPostNestedScrollConnection(
        if (
            isPriorityMode ||
                source == NestedScrollSource.Fling ||
                !canStart(available, offsetBeforeStart)
                !canStartPostScroll(available, offsetBeforeStart)
        ) {
            // The priority mode cannot start so we won't consume the available offset.
            return Offset.Zero
        }

        // Step 1: It's our turn! We start capturing scroll events when one of our children has an
        // available offset following a scroll event.
        isPriorityMode = true

        // Note: onStop will be called if we cannot continue to scroll (step 3a), or the finger is
        // lifted (step 3b), or this object has been destroyed (step 3c).
        onStart()

        return onScroll(available)
        return onPriorityStart(available)
    }

    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        if (!isPriorityMode) {
            if (source != NestedScrollSource.Fling) {
                if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) {
                    return onPriorityStart(available)
                }
                // We want to track the amount of offset consumed before entering priority mode
                offsetScrolledBeforePriorityMode += available
            }
@@ -87,6 +83,11 @@ class PriorityPostNestedScrollConnection(
        if (!canContinueScroll()) {
            // Step 3a: We have lost priority and we no longer need to intercept scroll events.
            onPriorityStop(velocity = Velocity.Zero)

            // We've just reset offsetScrolledBeforePriorityMode to Offset.Zero
            // We want to track the amount of offset consumed before entering priority mode
            offsetScrolledBeforePriorityMode += available

            return Offset.Zero
        }

@@ -110,8 +111,23 @@ class PriorityPostNestedScrollConnection(
        onPriorityStop(velocity = Velocity.Zero)
    }

    private fun onPriorityStop(velocity: Velocity): Velocity {
    private fun onPriorityStart(available: Offset): Offset {
        if (isPriorityMode) {
            error("This should never happen, onPriorityStart() was called when isPriorityMode")
        }

        // Step 1: It's our turn! We start capturing scroll events when one of our children has an
        // available offset following a scroll event.
        isPriorityMode = true

        // Note: onStop will be called if we cannot continue to scroll (step 3a), or the finger is
        // lifted (step 3b), or this object has been destroyed (step 3c).
        onStart()

        return onScroll(available)
    }

    private fun onPriorityStop(velocity: Velocity): Velocity {
        // We can restart tracking the consumed offsets from scratch.
        offsetScrolledBeforePriorityMode = Offset.Zero

+37 −14
Original line number Diff line number Diff line
@@ -29,8 +29,9 @@ import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PriorityPostNestedScrollConnectionTest {
    private var canStart = false
class PriorityNestedScrollConnectionTest {
    private var canStartPreScroll = false
    private var canStartPostScroll = false
    private var canContinueScroll = false
    private var isStarted = false
    private var lastScroll: Offset? = null
@@ -41,8 +42,9 @@ class PriorityPostNestedScrollConnectionTest {
    private var returnOnPostFling = Velocity.Zero

    private val scrollConnection =
        PriorityPostNestedScrollConnection(
            canStart = { _, _ -> canStart },
        PriorityNestedScrollConnection(
            canStartPreScroll = { _, _ -> canStartPreScroll },
            canStartPostScroll = { _, _ -> canStartPostScroll },
            canContinueScroll = { canContinueScroll },
            onStart = { isStarted = true },
            onScroll = {
@@ -64,8 +66,29 @@ class PriorityPostNestedScrollConnectionTest {
    private val velocity1 = Velocity(1f, 1f)
    private val velocity2 = Velocity(2f, 2f)

    private fun startPriorityMode() {
        canStart = true
    @Test
    fun step1_priorityModeShouldStartOnlyOnPreScroll() = runTest {
        canStartPreScroll = true

        scrollConnection.onPostScroll(
            consumed = Offset.Zero,
            available = Offset.Zero,
            source = NestedScrollSource.Drag
        )
        assertThat(isStarted).isEqualTo(false)

        scrollConnection.onPreFling(available = Velocity.Zero)
        assertThat(isStarted).isEqualTo(false)

        scrollConnection.onPostFling(consumed = Velocity.Zero, available = Velocity.Zero)
        assertThat(isStarted).isEqualTo(false)

        scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
        assertThat(isStarted).isEqualTo(true)
    }

    private fun startPriorityModePostScroll() {
        canStartPostScroll = true
        scrollConnection.onPostScroll(
            consumed = Offset.Zero,
            available = Offset.Zero,
@@ -75,7 +98,7 @@ class PriorityPostNestedScrollConnectionTest {

    @Test
    fun step1_priorityModeShouldStartOnlyOnPostScroll() = runTest {
        canStart = true
        canStartPostScroll = true

        scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
        assertThat(isStarted).isEqualTo(false)
@@ -86,7 +109,7 @@ class PriorityPostNestedScrollConnectionTest {
        scrollConnection.onPostFling(consumed = Velocity.Zero, available = Velocity.Zero)
        assertThat(isStarted).isEqualTo(false)

        startPriorityMode()
        startPriorityModePostScroll()
        assertThat(isStarted).isEqualTo(true)
    }

@@ -99,13 +122,13 @@ class PriorityPostNestedScrollConnectionTest {
        )
        assertThat(isStarted).isEqualTo(false)

        startPriorityMode()
        startPriorityModePostScroll()
        assertThat(isStarted).isEqualTo(true)
    }

    @Test
    fun step1_onPriorityModeStarted_receiveAvailableOffset() {
        canStart = true
        canStartPostScroll = true

        scrollConnection.onPostScroll(
            consumed = offset1,
@@ -118,7 +141,7 @@ class PriorityPostNestedScrollConnectionTest {

    @Test
    fun step2_onPriorityMode_shouldContinueIfAllowed() {
        startPriorityMode()
        startPriorityModePostScroll()
        canContinueScroll = true

        scrollConnection.onPreScroll(available = offset1, source = NestedScrollSource.Drag)
@@ -132,7 +155,7 @@ class PriorityPostNestedScrollConnectionTest {

    @Test
    fun step3a_onPriorityMode_shouldStopIfCannotContinue() {
        startPriorityMode()
        startPriorityModePostScroll()
        canContinueScroll = false

        scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
@@ -142,7 +165,7 @@ class PriorityPostNestedScrollConnectionTest {

    @Test
    fun step3b_onPriorityMode_shouldStopOnFling() = runTest {
        startPriorityMode()
        startPriorityModePostScroll()
        canContinueScroll = true

        scrollConnection.onPreFling(available = Velocity.Zero)
@@ -152,7 +175,7 @@ class PriorityPostNestedScrollConnectionTest {

    @Test
    fun step3c_onPriorityMode_shouldStopOnReset() {
        startPriorityMode()
        startPriorityModePostScroll()
        canContinueScroll = true

        scrollConnection.reset()