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

Commit 40c5c231 authored by omarmt's avatar omarmt
Browse files

Simplify PriorityNestedScrollConnection

To simplify using PriorityNestedScrollConnection, we've reduced the
number of properties.
To stop scrolling, simply don't consume (return 0) the value received
during onScroll.

Test: atest PriorityNestedScrollConnectionTest
Bug: 370949877
Flag: com.android.systemui.scene_container
Change-Id: I03f4344299093ba3ebcba96cbe50ae6527fdff21
parent 249f87d7
Loading
Loading
Loading
Loading
+8 −10
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.notifications.ui.composable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost
import com.android.compose.nestedscroll.PriorityNestedScrollConnection

/**
@@ -44,7 +46,7 @@ fun NotificationScrimNestedScrollConnection(
        orientation = Orientation.Vertical,
        // scrolling up and inner content is taller than the scrim, so scrim needs to
        // expand; content can scroll once scrim is at the minScrimOffset.
        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
        canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
            offsetAvailable < 0 &&
                offsetBeforeStart == 0f &&
                contentHeight() > minVisibleScrimHeight() &&
@@ -52,25 +54,21 @@ fun NotificationScrimNestedScrollConnection(
        },
        // scrolling down and content is done scrolling to top. After that, the scrim
        // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
        canStartPostScroll = { offsetAvailable, _ ->
        canStartPostScroll = { offsetAvailable, _, _ ->
            offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll())
        },
        canStartPostFling = { false },
        canContinueScroll = {
            val currentHeight = scrimOffset()
            minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
        },
        canScrollOnFling = true,
        canStopOnPreFling = { false },
        onStart = { offsetAvailable -> onStart(offsetAvailable) },
        onScroll = { offsetAvailable ->
        onScroll = { offsetAvailable, _ ->
            val currentHeight = scrimOffset()
            val amountConsumed =
                if (offsetAvailable > 0) {
                    val amountLeft = maxScrimOffset - currentHeight
                    offsetAvailable.coerceAtMost(amountLeft)
                    offsetAvailable.fastCoerceAtMost(amountLeft)
                } else {
                    val amountLeft = minScrimOffset() - currentHeight
                    offsetAvailable.coerceAtLeast(amountLeft)
                    offsetAvailable.fastCoerceAtLeast(amountLeft)
                }
            snapScrimOffset(currentHeight + amountConsumed)
            amountConsumed
+11 −7
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceAtLeast
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.max
import kotlin.math.roundToInt
@@ -86,17 +87,20 @@ fun NotificationStackNestedScrollConnection(
): PriorityNestedScrollConnection {
    return PriorityNestedScrollConnection(
        orientation = Orientation.Vertical,
        canStartPreScroll = { _, _ -> false },
        canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
        canStartPreScroll = { _, _, _ -> false },
        canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ ->
            offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
        },
        canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() },
        canContinueScroll = { stackOffset() > 0f },
        canScrollOnFling = true,
        canStopOnPreFling = { false },
        onStart = { offsetAvailable -> onStart(offsetAvailable) },
        onScroll = { offsetAvailable ->
            onScroll(offsetAvailable)
            offsetAvailable
        onScroll = { offsetAvailable, _ ->
            val minOffset = 0f
            val consumed = offsetAvailable.fastCoerceAtLeast(minOffset - stackOffset())
            if (consumed != 0f) {
                onScroll(consumed)
            }
            consumed
        },
        onStop = { velocityAvailable ->
            onStop(velocityAvailable)
+4 −5
Original line number Diff line number Diff line
@@ -612,7 +612,7 @@ internal class NestedScrollHandlerImpl(

        return PriorityNestedScrollConnection(
            orientation = orientation,
            canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
            canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
                canChangeScene =
                    if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f

@@ -638,7 +638,7 @@ internal class NestedScrollHandlerImpl(
                isIntercepting = true
                true
            },
            canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
            canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ ->
                val behavior: NestedScrollBehavior =
                    when {
                        offsetAvailable > 0f -> topOrLeftBehavior
@@ -693,8 +693,7 @@ internal class NestedScrollHandlerImpl(

                canStart
            },
            canContinueScroll = { true },
            canScrollOnFling = false,
            canStopOnPreFling = { true },
            onStart = { offsetAvailable ->
                val pointersInfo = pointersInfo()
                dragController =
@@ -704,7 +703,7 @@ internal class NestedScrollHandlerImpl(
                        overSlop = if (isIntercepting) 0f else offsetAvailable,
                    )
            },
            onScroll = { offsetAvailable ->
            onScroll = { offsetAvailable, _ ->
                val controller = dragController ?: error("Should be called after onStart")

                // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
+8 −10
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.compose.nestedscroll

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost

/**
 * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
@@ -43,30 +45,26 @@ fun LargeTopAppBarNestedScrollConnection(
        orientation = Orientation.Vertical,
        // When swiping up, the LargeTopAppBar will shrink (to [minHeight]) and the content will
        // expand. Then, you can then scroll down the content.
        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
        canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
            offsetAvailable < 0 && offsetBeforeStart == 0f && height() > minHeight()
        },
        // When swiping down, the content will scroll up until it reaches the top. Then, the
        // LargeTopAppBar will expand until it reaches its [maxHeight].
        canStartPostScroll = { offsetAvailable, _ ->
        canStartPostScroll = { offsetAvailable, _, _ ->
            offsetAvailable > 0 && height() < maxHeight()
        },
        canStartPostFling = { false },
        canContinueScroll = {
            val currentHeight = height()
            minHeight() < currentHeight && currentHeight < maxHeight()
        },
        canScrollOnFling = true,
        canStopOnPreFling = { false },
        onStart = { /* do nothing */ },
        onScroll = { offsetAvailable ->
        onScroll = { offsetAvailable, _ ->
            val currentHeight = height()
            val amountConsumed =
                if (offsetAvailable > 0) {
                    val amountLeft = maxHeight() - currentHeight
                    offsetAvailable.coerceAtMost(amountLeft)
                    offsetAvailable.fastCoerceAtMost(amountLeft)
                } else {
                    val amountLeft = minHeight() - currentHeight
                    offsetAvailable.coerceAtLeast(amountLeft)
                    offsetAvailable.fastCoerceAtLeast(amountLeft)
                }
            onHeightChanged(currentHeight + amountConsumed)
            amountConsumed
+94 −56
Original line number Diff line number Diff line
@@ -27,25 +27,39 @@ import kotlin.math.sign
internal typealias SuspendedValue<T> = suspend () -> T

/**
 * 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.
 * A [NestedScrollConnection] that intercepts scroll events in priority mode.
 *
 * Note: Call [reset] before destroying this object to make sure you always get a call to [onStop]
 * after [onStart].
 * Priority mode allows this connection to take control over scroll events within a nested scroll
 * hierarchy. When in priority mode, this connection consumes scroll events before its children,
 * enabling custom scrolling behaviors like sticky headers.
 *
 * @param orientation The orientation of the scroll.
 * @param canStartPreScroll lambda that returns true if the connection can start consuming scroll
 *   events in pre-scroll mode.
 * @param canStartPostScroll lambda that returns true if the connection can start consuming scroll
 *   events in post-scroll mode.
 * @param canStartPostFling lambda that returns true if the connection can start consuming scroll
 *   events in post-fling mode.
 * @param canStopOnPreFling lambda that returns true if the connection can stop consuming scroll
 *   events in pre-fling (i.e. as soon as the user lifts their fingers).
 * @param onStart lambda that is called when the connection starts consuming scroll events.
 * @param onScroll lambda that is called when the connection consumes a scroll event and returns the
 *   consumed amount.
 * @param onStop lambda that is called when the connection stops consuming scroll events and returns
 *   the consumed velocity.
 * @sample LargeTopAppBarNestedScrollConnection
 * @sample com.android.compose.animation.scene.NestedScrollHandlerImpl.nestedScrollConnection
 */
class PriorityNestedScrollConnection(
    orientation: Orientation,
    private val canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
    private val canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
    private val canStartPreScroll:
        (offsetAvailable: Float, offsetBeforeStart: Float, source: NestedScrollSource) -> Boolean,
    private val canStartPostScroll:
        (offsetAvailable: Float, offsetBeforeStart: Float, source: NestedScrollSource) -> Boolean,
    private val canStartPostFling: (velocityAvailable: Float) -> Boolean,
    private val canContinueScroll: (source: NestedScrollSource) -> Boolean,
    private val canScrollOnFling: Boolean,
    private val canStopOnPreFling: () -> Boolean,
    private val onStart: (offsetAvailable: Float) -> Unit,
    private val onScroll: (offsetAvailable: Float) -> Float,
    private val onScroll: (offsetAvailable: Float, source: NestedScrollSource) -> Float,
    private val onStop: (velocityAvailable: Float) -> SuspendedValue<Float>,
) : NestedScrollConnection, SpaceVectorConverter by SpaceVectorConverter(orientation) {

@@ -64,62 +78,48 @@ class PriorityNestedScrollConnection(
        // the beginning or from the last fling gesture.
        val offsetBeforeStart = offsetScrolledBeforePriorityMode - availableFloat

        if (
            isPriorityMode ||
                (source == NestedScrollSource.SideEffect && !canScrollOnFling) ||
                !canStartPostScroll(availableFloat, offsetBeforeStart)
        ) {
        if (isPriorityMode || !canStartPostScroll(availableFloat, offsetBeforeStart, source)) {
            // The priority mode cannot start so we won't consume the available offset.
            return Offset.Zero
        }

        return onPriorityStart(availableFloat).toOffset()
        return start(availableFloat, source).toOffset()
    }

    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        if (!isPriorityMode) {
            if (source == NestedScrollSource.UserInput || canScrollOnFling) {
            val availableFloat = available.toFloat()
                if (canStartPreScroll(availableFloat, offsetScrolledBeforePriorityMode)) {
                    return onPriorityStart(availableFloat).toOffset()
            if (canStartPreScroll(availableFloat, offsetScrolledBeforePriorityMode, source)) {
                return start(availableFloat, source).toOffset()
            }
            // We want to track the amount of offset consumed before entering priority mode
            offsetScrolledBeforePriorityMode += availableFloat
            }

            return Offset.Zero
        }

        val availableFloat = available.toFloat()
        if (!canContinueScroll(source)) {
            // Step 3a: We have lost priority and we no longer need to intercept scroll events.
            onPriorityStop(velocity = 0f)

            // We've just reset offsetScrolledBeforePriorityMode to 0f
            // We want to track the amount of offset consumed before entering priority mode
            offsetScrolledBeforePriorityMode += availableFloat
        return scroll(available.toFloat(), source).toOffset()
    }

            return Offset.Zero
    override suspend fun onPreFling(available: Velocity): Velocity {
        if (!isPriorityMode) {
            resetOffsetTracker()
            return Velocity.Zero
        }

        // Step 2: We have the priority and can consume the scroll events.
        return onScroll(availableFloat).toOffset()
        if (canStopOnPreFling()) {
            // Step 3b: The finger is lifted, we can stop intercepting scroll events and use the
            // velocity of the fling gesture.
            return stop(velocityAvailable = available.toFloat()).toVelocity()
        }

    override suspend fun onPreFling(available: Velocity): Velocity {
        if (isPriorityMode && canScrollOnFling) {
        // We don't want to consume the velocity, we prefer to continue receiving scroll events.
        return Velocity.Zero
    }
        // Step 3b: The finger is lifted, we can stop intercepting scroll events and use the speed
        // of the fling gesture.
        return onPriorityStop(velocity = available.toFloat()).invoke().toVelocity()
    }

    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        val availableFloat = available.toFloat()
        if (isPriorityMode) {
            return onPriorityStop(velocity = availableFloat).invoke().toVelocity()
            return stop(velocityAvailable = availableFloat).toVelocity()
        }

        if (!canStartPostFling(availableFloat)) {
@@ -131,10 +131,14 @@ class PriorityNestedScrollConnection(
        // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the
        // overscroll behavior on the Scene level.
        val smallOffset = availableFloat.sign
        onPriorityStart(availableOffset = smallOffset)
        start(
            availableOffset = smallOffset,
            source = NestedScrollSource.SideEffect,
            skipScroll = true,
        )

        // This is the last event of a scroll gesture.
        return onPriorityStop(availableFloat).invoke().toVelocity()
        return stop(availableFloat).toVelocity()
    }

    /**
@@ -143,13 +147,25 @@ class PriorityNestedScrollConnection(
     * TODO(b/303224944) This method should be removed.
     */
    fun reset() {
        if (isPriorityMode) {
            // Step 3c: To ensure that an onStop is always called for every onStart.
        onPriorityStop(velocity = 0f)
            cancel()
        } else {
            resetOffsetTracker()
        }
    }

    private fun onPriorityStart(availableOffset: Float): Float {
        if (isPriorityMode) {
            error("This should never happen, onPriorityStart() was called when isPriorityMode")
    private fun shouldStop(consumed: Float): Boolean {
        return consumed == 0f
    }

    private fun start(
        availableOffset: Float,
        source: NestedScrollSource,
        skipScroll: Boolean = false,
    ): Float {
        check(!isPriorityMode) {
            "This should never happen, start() was called when isPriorityMode"
        }

        // Step 1: It's our turn! We start capturing scroll events when one of our children has an
@@ -160,19 +176,41 @@ class PriorityNestedScrollConnection(
        // lifted (step 3b), or this object has been destroyed (step 3c).
        onStart(availableOffset)

        return onScroll(availableOffset)
        return if (skipScroll) 0f else scroll(availableOffset, source)
    }

    private fun onPriorityStop(velocity: Float): SuspendedValue<Float> {
        // We can restart tracking the consumed offsets from scratch.
        offsetScrolledBeforePriorityMode = 0f
    private fun scroll(offsetAvailable: Float, source: NestedScrollSource): Float {
        // Step 2: We have the priority and can consume the scroll events.
        val consumedByScroll = onScroll(offsetAvailable, source)

        if (!isPriorityMode) {
            return { 0f }
        if (shouldStop(consumedByScroll)) {
            // Step 3a: We have lost priority and we no longer need to intercept scroll events.
            cancel()

            // We've just reset offsetScrolledBeforePriorityMode to 0f
            // We want to track the amount of offset consumed before entering priority mode
            offsetScrolledBeforePriorityMode += offsetAvailable - consumedByScroll
        }

        return consumedByScroll
    }

    /** Reset the tracking of consumed offsets before entering in priority mode. */
    private fun resetOffsetTracker() {
        offsetScrolledBeforePriorityMode = 0f
    }

    private suspend fun stop(velocityAvailable: Float): Float {
        check(isPriorityMode) { "This should never happen, stop() was called before start()" }
        isPriorityMode = false
        resetOffsetTracker()
        return onStop(velocityAvailable).invoke()
    }

        return onStop(velocity)
    private fun cancel() {
        check(isPriorityMode) { "This should never happen, cancel() was called before start()" }
        isPriorityMode = false
        resetOffsetTracker()
        onStop(0f)
    }
}
Loading