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

Commit 9ec83fe1 authored by Omar Miatello's avatar Omar Miatello Committed by Android (Google) Code Review
Browse files

Merge "NestedScrollToScene communicate desired behavior while scrolling" into main

parents fa66973d b29945a6
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ internal class DraggableHandlerImpl(
    internal val orientation: Orientation,
    internal val coroutineScope: CoroutineScope,
) : DraggableHandler {
    internal val nestedScrollKey = Any()
    /** The [DraggableHandler] can only have one active [DragController] at a time. */
    private var dragController: DragControllerImpl? = null

+64 −5
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.compose.animation.scene

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
@@ -122,13 +125,55 @@ private data class NestedScrollToSceneElement(
}

private class NestedScrollToSceneNode(
    layoutImpl: SceneTransitionLayoutImpl,
    orientation: Orientation,
    topOrLeftBehavior: NestedScrollBehavior,
    bottomOrRightBehavior: NestedScrollBehavior,
    isExternalOverscrollGesture: () -> Boolean,
    private var layoutImpl: SceneTransitionLayoutImpl,
    private var orientation: Orientation,
    private var topOrLeftBehavior: NestedScrollBehavior,
    private var bottomOrRightBehavior: NestedScrollBehavior,
    private var isExternalOverscrollGesture: () -> Boolean,
) : DelegatingNode() {
    lateinit var pointersInfoOwner: PointersInfoOwner
    private var scrollBehaviorOwner: ScrollBehaviorOwner? = null

    private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner {
        var behaviorOwner = scrollBehaviorOwner
        if (behaviorOwner == null) {
            behaviorOwner = requireScrollBehaviorOwner(layoutImpl.draggableHandler(orientation))
            scrollBehaviorOwner = behaviorOwner
        }
        return behaviorOwner
    }

    val updateScrollBehaviorsConnection =
        object : NestedScrollConnection {
            /**
             * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior
             * before our parent components. This gives them the option to override our behavior if
             * they choose.
             *
             * The behavior can be communicated at every scroll gesture to ensure that the hierarchy
             * is respected, even if one of our descendant nodes changes behavior after we set it.
             */
            override fun onPostScroll(
                consumed: Offset,
                available: Offset,
                source: NestedScrollSource,
            ): Offset {
                // If we have some remaining scroll, that scroll can be used to initiate a
                // transition between scenes. We can assume that the behavior is only needed if
                // there is some remaining amount.
                if (available != Offset.Zero) {
                    requireScrollBehaviorOwner()
                        .updateScrollBehaviors(
                            topOrLeftBehavior = topOrLeftBehavior,
                            bottomOrRightBehavior = bottomOrRightBehavior,
                            isExternalOverscrollGesture = isExternalOverscrollGesture,
                        )
                }

                return Offset.Zero
            }
        }

    private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
        scenePriorityNestedScrollConnection(
            layoutImpl = layoutImpl,
@@ -145,14 +190,22 @@ private class NestedScrollToSceneNode(
            dispatcher = null,
        )

    private var updateScrollBehaviorsNestedScrollNode: DelegatableNode =
        nestedScrollModifierNode(
            connection = updateScrollBehaviorsConnection,
            dispatcher = null,
        )

    override fun onAttach() {
        pointersInfoOwner = requireAncestorPointersInfoOwner()
        delegate(nestedScrollNode)
        delegate(updateScrollBehaviorsNestedScrollNode)
    }

    override fun onDetach() {
        // Make sure we reset the scroll connection when this modifier is removed from composition
        priorityNestedScrollConnection.reset()
        scrollBehaviorOwner = null
    }

    fun update(
@@ -162,6 +215,12 @@ private class NestedScrollToSceneNode(
        bottomOrRightBehavior: NestedScrollBehavior,
        isExternalOverscrollGesture: () -> Boolean,
    ) {
        this.layoutImpl = layoutImpl
        this.orientation = orientation
        this.topOrLeftBehavior = topOrLeftBehavior
        this.bottomOrRightBehavior = bottomOrRightBehavior
        this.isExternalOverscrollGesture = isExternalOverscrollGesture

        // Clean up the old nested scroll connection
        priorityNestedScrollConnection.reset()
        undelegate(nestedScrollNode)
+48 −5
Original line number Diff line number Diff line
@@ -22,9 +22,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.unit.IntSize

/**
@@ -53,7 +56,7 @@ private class SwipeToSceneNode(
    draggableHandler: DraggableHandlerImpl,
    swipeDetector: SwipeDetector,
) : DelegatingNode(), PointerInputModifierNode {
    private val delegate =
    private val multiPointerDraggableNode =
        delegate(
            MultiPointerDraggableNode(
                orientation = draggableHandler.orientation,
@@ -74,21 +77,25 @@ private class SwipeToSceneNode(
                // Make sure to update the delegate orientation. Note that this will automatically
                // reset the underlying pointer input handler, so previous gestures will be
                // cancelled.
                delegate.orientation = value.orientation
                multiPointerDraggableNode.orientation = value.orientation
            }
        }

    override fun onAttach() {
        delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey))
    }

    override fun onPointerEvent(
        pointerEvent: PointerEvent,
        pass: PointerEventPass,
        bounds: IntSize,
    ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
    ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds)

    override fun onCancelPointerInput() = delegate.onCancelPointerInput()
    override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()

    private fun enabled(): Boolean {
        return draggableHandler.isDrivingTransition ||
            currentScene().shouldEnableSwipes(delegate.orientation)
            currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation)
    }

    private fun currentScene(): Scene {
@@ -118,3 +125,39 @@ private class SwipeToSceneNode(
        return currentScene().shouldEnableSwipes(oppositeOrientation)
    }
}

/** Find the [ScrollBehaviorOwner] for the current orientation. */
internal fun DelegatableNode.requireScrollBehaviorOwner(
    draggableHandler: DraggableHandlerImpl
): ScrollBehaviorOwner {
    val ancestorNode =
        checkNotNull(findNearestAncestor(draggableHandler.nestedScrollKey)) {
            "This should never happen! Couldn't find a ScrollBehaviorOwner. " +
                "Are we inside an SceneTransitionLayout?"
        }
    return ancestorNode as ScrollBehaviorOwner
}

internal fun interface ScrollBehaviorOwner {
    fun updateScrollBehaviors(
        topOrLeftBehavior: NestedScrollBehavior,
        bottomOrRightBehavior: NestedScrollBehavior,
        isExternalOverscrollGesture: () -> Boolean,
    )
}

/**
 * We need a node that receives the desired behavior.
 *
 * TODO(b/353234530) move this logic into [SwipeToSceneNode]
 */
private class ScrollBehaviorOwnerNode(override val traverseKey: Any) :
    Modifier.Node(), TraversableNode, ScrollBehaviorOwner {
    override fun updateScrollBehaviors(
        topOrLeftBehavior: NestedScrollBehavior,
        bottomOrRightBehavior: NestedScrollBehavior,
        isExternalOverscrollGesture: () -> Boolean
    ) {
        // This method will be used to update the desired behavior in a following CL.
    }
}