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

Commit cf92b9b3 authored by omarmt's avatar omarmt
Browse files

Add ScrollController in PriorityNestedScrollConnection

Now, `onStart` needs to provides a `ScrollController` to manage the
scrolling.

A PriorityNestedScrollConnection ensures only one ScrollController is
active at a time, guaranteeing methods are called in the correct
sequence.

The ScrollController provides control over the scroll gesture.
It allows you to:
- Scroll the content by a given pixel amount.
- Stop the scrolling with a given initial velocity.
- Cancel the current scroll operation.

Test: Refactor. Manually tested on the demo app and Flexiglass
Bug: 371984715
Flag: com.android.systemui.scene_container
Change-Id: I86724b73f4178995c39d167daea0a7de6370f7d4
parent 80d63f34
Loading
Loading
Loading
Loading
+35 −26
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ package com.android.systemui.notifications.ui.composable

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

/**
 * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
@@ -58,34 +60,41 @@ fun NotificationScrimNestedScrollConnection(
            offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll())
        },
        canStartPostFling = { false },
        canStopOnPreFling = { false },
        onStart = { offsetAvailable -> onStart(offsetAvailable) },
        onScroll = { offsetAvailable, _ ->
        onStart = { firstScroll ->
            onStart(firstScroll)
            object : ScrollController {
                override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
                    val currentHeight = scrimOffset()
                    val amountConsumed =
                if (offsetAvailable > 0) {
                        if (deltaScroll > 0) {
                            val amountLeft = maxScrimOffset - currentHeight
                    offsetAvailable.fastCoerceAtMost(amountLeft)
                            deltaScroll.fastCoerceAtMost(amountLeft)
                        } else {
                            val amountLeft = minScrimOffset() - currentHeight
                    offsetAvailable.fastCoerceAtLeast(amountLeft)
                            deltaScroll.fastCoerceAtLeast(amountLeft)
                        }
                    snapScrimOffset(currentHeight + amountConsumed)
            amountConsumed
        },
        onStop = { velocityAvailable ->
            onStop(velocityAvailable)
                    return amountConsumed
                }

                override suspend fun onStop(initialVelocity: Float): Float {
                    onStop(initialVelocity)
                    if (scrimOffset() < minScrimOffset()) {
                        animateScrimOffset(minScrimOffset())
                    }
                    // Don't consume the velocity on pre/post fling
            0f
        },
        onCancel = {
                    return 0f
                }

                override fun onCancel() {
                    onStop(0f)
                    if (scrimOffset() < minScrimOffset()) {
                        animateScrimOffset(minScrimOffset())
                    }
                }

                override fun canStopOnPreFling() = false
            }
        },
    )
}
+24 −13
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
@@ -30,6 +31,7 @@ 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 com.android.compose.nestedscroll.ScrollController
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.tanh
@@ -92,20 +94,29 @@ fun NotificationStackNestedScrollConnection(
            offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
        },
        canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() },
        canStopOnPreFling = { false },
        onStart = { offsetAvailable -> onStart(offsetAvailable) },
        onScroll = { offsetAvailable, _ ->
        onStart = { firstScroll ->
            onStart(firstScroll)
            object : ScrollController {
                override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
                    val minOffset = 0f
            val consumed = offsetAvailable.fastCoerceAtLeast(minOffset - stackOffset())
                    val consumed = deltaScroll.fastCoerceAtLeast(minOffset - stackOffset())
                    if (consumed != 0f) {
                        onScroll(consumed)
                    }
            consumed
        },
        onStop = { velocityAvailable ->
            onStop(velocityAvailable)
            velocityAvailable
                    return consumed
                }

                override suspend fun onStop(initialVelocity: Float): Float {
                    onStop(initialVelocity)
                    return initialVelocity
                }

                override fun onCancel() {
                    onStop(0f)
                }

                override fun canStopOnPreFling() = false
            }
        },
        onCancel = { onStop(0f) },
    )
}
+53 −44
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package com.android.compose.animation.scene

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
@@ -27,6 +28,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.ScrollController
import kotlin.math.absoluteValue

internal typealias SuspendedValue<T> = suspend () -> T
@@ -66,6 +68,7 @@ internal class DraggableHandlerImpl(
    internal val orientation: Orientation,
) : DraggableHandler {
    internal val nestedScrollKey = Any()

    /** The [DraggableHandler] can only have one active [DragController] at a time. */
    private var dragController: DragControllerImpl? = null

@@ -345,6 +348,7 @@ private class DragControllerImpl(
                    distance == DistanceUnspecified ||
                        swipeAnimation.contentTransition.isWithinProgressRange(desiredProgress) ->
                        desiredOffset

                    distance > 0f -> desiredOffset.fastCoerceIn(0f, distance)
                    else -> desiredOffset.fastCoerceIn(distance, 0f)
                }
@@ -545,6 +549,7 @@ internal class Swipes(
            upOrLeftResult == null && downOrRightResult == null -> null
            (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
                upOrLeftResult

            else -> downOrRightResult
        }
    }
@@ -608,7 +613,6 @@ internal class NestedScrollHandlerImpl(
            return overscrollSpec != null
        }

        var dragController: DragController? = null
        var isIntercepting = false

        return PriorityNestedScrollConnection(
@@ -669,10 +673,12 @@ internal class NestedScrollHandlerImpl(
                            canChangeScene = isZeroOffset
                            isZeroOffset && hasNextScene(offsetAvailable)
                        }

                        NestedScrollBehavior.EdgeWithPreview -> {
                            canChangeScene = isZeroOffset
                            hasNextScene(offsetAvailable)
                        }

                        NestedScrollBehavior.EdgeAlways -> {
                            canChangeScene = true
                            hasNextScene(offsetAvailable)
@@ -710,53 +716,56 @@ internal class NestedScrollHandlerImpl(

                canStart
            },
            // We need to maintain scroll priority even if the scene transition can no longer
            // consume the scroll gesture to allow us to return to the previous scene.
            canStopOnScroll = { _, _ -> false },
            canStopOnPreFling = { true },
            onStart = { offsetAvailable ->
            onStart = { firstScroll ->
                val pointersInfo = pointersInfo()
                scrollController(
                    dragController =
                        draggableHandler.onDragStarted(
                            pointersDown = pointersInfo.pointersDown,
                            startedPosition = pointersInfo.startedPosition,
                        overSlop = if (isIntercepting) 0f else offsetAvailable,
                            overSlop = if (isIntercepting) 0f else firstScroll,
                        ),
                    canChangeScene = canChangeScene,
                    pointersInfoOwner = pointersInfoOwner,
                )
            },
            onScroll = { offsetAvailable, _ ->
                val controller = dragController ?: error("Should be called after onStart")
        )
    }
}

private fun scrollController(
    dragController: DragController,
    canChangeScene: Boolean,
    pointersInfoOwner: PointersInfoOwner,
): ScrollController {
    return object : ScrollController {
        override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
            val pointersInfo = pointersInfoOwner.pointersInfo()
            if (pointersInfo.isMouseWheel) {
                // Do not support mouse wheel interactions
                    return@PriorityNestedScrollConnection 0f
                return 0f
            }

                // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
                // initiated in a nested child.
                controller.onDrag(delta = offsetAvailable)
            },
            onStop = { velocityAvailable ->
                val controller = dragController ?: error("Should be called after onStart")
                try {
                    controller
                        .onStop(velocity = velocityAvailable, canChangeContent = canChangeScene)
            return dragController.onDrag(delta = deltaScroll)
        }

        override suspend fun onStop(initialVelocity: Float): Float {
            return dragController
                .onStop(velocity = initialVelocity, canChangeContent = canChangeScene)
                .invoke()
                } finally {
                    // onStop might still be running when a new gesture begins.
                    // To prevent conflicts, we should only remove the drag controller if it's the
                    // same one that was active initially.
                    if (dragController == controller) {
                        dragController = null
        }

        override fun onCancel() {
            dragController.onStop(velocity = 0f, canChangeContent = canChangeScene)
        }
            },
            onCancel = {
                val controller = dragController ?: error("Should be called after onStart")
                controller.onStop(velocity = 0f, canChangeContent = canChangeScene)
                dragController = null
            },
        )

        /**
         * We need to maintain scroll priority even if the scene transition can no longer consume
         * the scroll gesture to allow us to return to the previous scene.
         */
        override fun canCancelScroll(available: Float, consumed: Float) = false

        override fun canStopOnPreFling() = true
    }
}

+34 −18
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.compose.nestedscroll

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

@@ -54,23 +55,38 @@ fun LargeTopAppBarNestedScrollConnection(
            offsetAvailable > 0 && height() < maxHeight()
        },
        canStartPostFling = { false },
        canStopOnPreFling = { false },
        onStart = { /* do nothing */ },
        onScroll = { offsetAvailable, _ ->
        onStart = { LargeTopAppBarScrollController(height, maxHeight, minHeight, onHeightChanged) },
    )
}

private class LargeTopAppBarScrollController(
    val height: () -> Float,
    val maxHeight: () -> Float,
    val minHeight: () -> Float,
    val onHeightChanged: (Float) -> Unit,
) : ScrollController {
    override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
        val currentHeight = height()
        val amountConsumed =
                if (offsetAvailable > 0) {
            if (deltaScroll > 0) {
                val amountLeft = maxHeight() - currentHeight
                    offsetAvailable.fastCoerceAtMost(amountLeft)
                deltaScroll.fastCoerceAtMost(amountLeft)
            } else {
                val amountLeft = minHeight() - currentHeight
                    offsetAvailable.fastCoerceAtLeast(amountLeft)
                deltaScroll.fastCoerceAtLeast(amountLeft)
            }
        onHeightChanged(currentHeight + amountConsumed)
            amountConsumed
        },
        return amountConsumed
    }

    override suspend fun onStop(initialVelocity: Float): Float {
        // Don't consume the velocity on pre/post fling
        onStop = { 0f },
        onCancel = { /* do nothing */ },
    )
        return 0f
    }

    override fun onCancel() {
        // do nothing
    }

    override fun canStopOnPreFling() = false
}
+234 −130

File changed.

Preview size limit exceeded, changes collapsed.

Loading