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

Commit ec8e10db authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Use NestedDraggable to handle STL gestures" into main

parents ff44c1d0 93920ee4
Loading
Loading
Loading
Loading
+150 −229
Original line number Diff line number Diff line
@@ -16,62 +16,29 @@

package com.android.compose.animation.scene

import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState.Companion.DistanceUnspecified
import com.android.compose.nestedscroll.OnStopScope
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import com.android.compose.nestedscroll.ScrollController
import com.android.compose.animation.scene.effect.GestureEffect
import com.android.compose.gesture.NestedDraggable
import com.android.compose.ui.util.SpaceVectorConverter
import kotlin.math.absoluteValue
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

internal interface DraggableHandler {
    /**
     * Start a drag with the given [pointersDown] and [overSlop].
     *
     * The returned [DragController] should be used to continue or stop the drag.
     */
    fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController
}

/**
 * The [DragController] provides control over the transition between two scenes through the [onDrag]
 * and [onStop] methods.
 */
internal interface DragController {
    /**
     * Drag the current scene by [delta] pixels.
     *
     * @param delta The distance to drag the scene in pixels.
     * @return the consumed [delta]
     */
    fun onDrag(delta: Float): Float

    /**
     * Stop the current drag with the given [velocity].
     *
     * @param velocity The velocity of the drag when it stopped.
     * @return the consumed [velocity] when the animation complete
     */
    suspend fun onStop(velocity: Float): Float

    /** Cancels the current drag. */
    fun onCancel()
}

internal class DraggableHandlerImpl(
internal class DraggableHandler(
    internal val layoutImpl: SceneTransitionLayoutImpl,
    internal val orientation: Orientation,
) : DraggableHandler {
    private val gestureEffectProvider: (ContentKey) -> GestureEffect,
) : NestedDraggable {
    /** The [DraggableHandler] can only have one active [DragController] at a time. */
    private var dragController: DragControllerImpl? = null

@@ -92,20 +59,36 @@ internal class DraggableHandlerImpl(
    internal val positionalThreshold
        get() = with(layoutImpl.density) { 56.dp.toPx() }

    /** The [OverscrollEffect] that should consume any overscroll on this draggable. */
    internal val overscrollEffect: OverscrollEffect = DelegatingOverscrollEffect()

    override fun shouldStartDrag(change: PointerInputChange): Boolean {
        return layoutImpl.swipeDetector.detectSwipe(change)
    }

    override fun shouldConsumeNestedScroll(sign: Float): Boolean {
        return this.enabled()
    }

    override fun onDragStarted(
        pointersDown: PointersInfo.PointersDown?,
        overSlop: Float,
    ): DragController {
        check(overSlop != 0f)
        val swipes = computeSwipes(pointersDown)
        position: Offset,
        sign: Float,
        pointersDown: Int,
        pointerType: PointerType?,
    ): NestedDraggable.Controller {
        check(sign != 0f)
        val swipes = computeSwipes(position, pointersDown, pointerType)
        val fromContent = layoutImpl.contentForUserActions()

        swipes.updateSwipesResults(fromContent)
        val upOrLeft = swipes.upOrLeftResult
        val downOrRight = swipes.downOrRightResult
        val result =
            (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult)
                // As we were unable to locate a valid target scene, the initial SwipeAnimation
                // cannot be defined. Consequently, a simple NoOp Controller will be returned.
                ?: return NoOpDragController
            when {
                sign < 0 -> upOrLeft ?: downOrRight
                sign >= 0f -> downOrRight ?: upOrLeft
                else -> null
            } ?: return NoOpDragController

        val swipeAnimation = createSwipeAnimation(swipes, result)
        return updateDragController(swipes, swipeAnimation)
@@ -143,20 +126,109 @@ internal class DraggableHandlerImpl(
        )
    }

    private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes {
        val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) }
    private fun computeSwipes(
        position: Offset,
        pointersDown: Int,
        pointerType: PointerType?,
    ): Swipes {
        val fromSource = resolveSwipeSource(position)
        return Swipes(
            upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource),
            downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource),
            upOrLeft =
                resolveSwipe(orientation, isUpOrLeft = true, fromSource, pointersDown, pointerType),
            downOrRight =
                resolveSwipe(orientation, isUpOrLeft = false, fromSource, pointersDown, pointerType),
        )
    }

    /**
     * An implementation of [OverscrollEffect] that delegates to the correct content effect
     * depending on the current scene/overlays and transition.
     */
    private inner class DelegatingOverscrollEffect :
        OverscrollEffect, SpaceVectorConverter by SpaceVectorConverter(orientation) {
        private var currentContent: ContentKey? = null
        private var currentDelegate: GestureEffect? = null
            set(value) {
                field?.let { delegate ->
                    if (delegate.isInProgress) {
                        layoutImpl.animationScope.launch { delegate.ensureApplyToFlingIsCalled() }
                    }
                }

                field = value
            }

        override val isInProgress: Boolean
            get() = currentDelegate?.isInProgress ?: false

        override fun applyToScroll(
            delta: Offset,
            source: NestedScrollSource,
            performScroll: (Offset) -> Offset,
        ): Offset {
            val available = delta.toFloat()
            if (available == 0f) {
                return performScroll(delta)
            }

            ensureDelegateIsNotNull(available)
            val delegate = checkNotNull(currentDelegate)
            return if (delegate.node.node.isAttached) {
                delegate.applyToScroll(delta, source, performScroll)
            } else {
                performScroll(delta)
            }
        }

        override suspend fun applyToFling(
            velocity: Velocity,
            performFling: suspend (Velocity) -> Velocity,
        ) {
            val available = velocity.toFloat()
            if (available != 0f && isDrivingTransition) {
                ensureDelegateIsNotNull(available)
            }

            // Note: we set currentDelegate and currentContent to null before calling performFling,
            // which can suspend and take a lot of time.
            val delegate = currentDelegate
            currentDelegate = null
            currentContent = null

            if (delegate != null && delegate.node.node.isAttached) {
                delegate.applyToFling(velocity, performFling)
            } else {
                performFling(velocity)
            }
        }

        private fun ensureDelegateIsNotNull(direction: Float) {
            require(direction != 0f)
            if (isInProgress) {
                return
            }

            val content =
                if (isDrivingTransition) {
                    checkNotNull(dragController).swipeAnimation.contentByDirection(direction)
                } else {
                    layoutImpl.contentForUserActions().key
                }

            if (content != currentContent) {
                currentContent = content
                currentDelegate = gestureEffectProvider(content)
            }
        }
    }
}

private fun resolveSwipe(
    orientation: Orientation,
    isUpOrLeft: Boolean,
    pointersDown: PointersInfo.PointersDown?,
    fromSource: SwipeSource.Resolved?,
    pointersDown: Int,
    pointerType: PointerType?,
): Swipe.Resolved {
    return Swipe.Resolved(
        direction =
@@ -175,28 +247,22 @@ private fun resolveSwipe(
                        SwipeDirection.Resolved.Down
                    }
            },
        // If the number of pointers is not specified, 1 is assumed.
        pointerCount = pointersDown?.count ?: 1,
        // Resolves the pointer type only if all pointers are of the same type.
        pointersType = pointersDown?.countByType?.keys?.singleOrNull(),
        pointerCount = pointersDown,
        pointerType = pointerType,
        fromSource = fromSource,
    )
}

/** @param swipes The [Swipes] associated to the current gesture. */
private class DragControllerImpl(
    private val draggableHandler: DraggableHandlerImpl,
    private val draggableHandler: DraggableHandler,
    val swipes: Swipes,
    var swipeAnimation: SwipeAnimation<*>,
) : DragController, SpaceVectorConverter by SpaceVectorConverter(draggableHandler.orientation) {
) :
    NestedDraggable.Controller,
    SpaceVectorConverter by SpaceVectorConverter(draggableHandler.orientation) {
    val layoutState = draggableHandler.layoutImpl.state

    val overscrollableContent: OverscrollableContent =
        when (draggableHandler.orientation) {
            Orientation.Vertical -> draggableHandler.layoutImpl.verticalOverscrollableContent
            Orientation.Horizontal -> draggableHandler.layoutImpl.horizontalOverscrollableContent
        }

    /**
     * Whether this handle is active. If this returns false, calling [onDrag] and [onStop] will do
     * nothing.
@@ -231,57 +297,25 @@ private class DragControllerImpl(
        if (delta == 0f || !isDrivingTransition || initialAnimation.isAnimatingOffset()) {
            return 0f
        }

        // swipeAnimation can change during the gesture, we want to always use the initial reference
        // during the whole drag gesture.
        return dragWithOverscroll(delta, animation = initialAnimation)
    }

    private fun <T : ContentKey> dragWithOverscroll(
        delta: Float,
        animation: SwipeAnimation<T>,
    ): Float {
        require(delta != 0f) { "delta should not be 0" }
        var overscrollEffect = overscrollableContent.currentOverscrollEffect

        // If we're already overscrolling, continue with the current effect for a smooth finish.
        if (overscrollEffect == null || !overscrollEffect.isInProgress) {
            // Otherwise, determine the target content (toContent or fromContent) for the new
            // overscroll effect based on the gesture's direction.
            val content = animation.contentByDirection(delta)
            overscrollEffect = overscrollableContent.applyOverscrollEffectOn(content)
        }

        // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
        if (!overscrollEffect.node.node.isAttached) {
            return drag(delta, animation)
        }

        return overscrollEffect
            .applyToScroll(
                delta = delta.toOffset(),
                source = NestedScrollSource.UserInput,
                performScroll = {
                    val preScrollAvailable = it.toFloat()
                    drag(preScrollAvailable, animation).toOffset()
                },
            )
            .toFloat()
        return drag(delta, animation = initialAnimation)
    }

    private fun <T : ContentKey> drag(delta: Float, animation: SwipeAnimation<T>): Float {
        if (delta == 0f) return 0f

        val distance = animation.distance()
        val previousOffset = animation.dragOffset
        val desiredOffset = previousOffset + delta
        val desiredProgress = animation.computeProgress(desiredOffset)

        // Note: the distance could be negative if fromContent is above or to the left of toContent.
        val newOffset =
            when {
                distance == DistanceUnspecified ||
                    animation.contentTransition.isWithinProgressRange(desiredProgress) ->
                    desiredOffset
                distance == DistanceUnspecified -> {
                    // Consume everything so that we don't overscroll, this will be coerced later
                    // when the distance is defined.
                    delta
                }
                distance > 0f -> desiredOffset.fastCoerceIn(0f, distance)
                else -> desiredOffset.fastCoerceIn(distance, 0f)
            }
@@ -290,12 +324,8 @@ private class DragControllerImpl(
        return newOffset - previousOffset
    }

    override suspend fun onStop(velocity: Float): Float {
        // To ensure that any ongoing animation completes gracefully and avoids an undefined state,
        // we execute the actual `onStop` logic in a non-cancellable context. This prevents the
        // coroutine from being cancelled prematurely, which could interrupt the animation.
        // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
        return withContext(NonCancellable) { onStop(velocity, swipeAnimation) }
    override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float {
        return onStop(velocity, swipeAnimation, awaitFling)
    }

    private suspend fun <T : ContentKey> onStop(
@@ -306,6 +336,7 @@ private class DragControllerImpl(
        // callbacks (like onAnimationCompleted()) might incorrectly finish a new transition that
        // replaced this one.
        swipeAnimation: SwipeAnimation<T>,
        awaitFling: suspend () -> Unit,
    ): Float {
        // The state was changed since the drag started; don't do anything.
        if (!isDrivingTransition || swipeAnimation.isAnimatingOffset()) {
@@ -337,33 +368,7 @@ private class DragControllerImpl(
                fromContent
            }

        val overscrollEffect = overscrollableContent.applyOverscrollEffectOn(targetContent)

        // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
        if (!overscrollEffect.node.node.isAttached) {
            return swipeAnimation.animateOffset(velocity, targetContent)
        }

        val overscrollCompletable = CompletableDeferred<Unit>()
        try {
            overscrollEffect.applyToFling(
                velocity = velocity.toVelocity(),
                performFling = {
                    val velocityLeft = it.toFloat()
                    swipeAnimation
                        .animateOffset(
                            velocityLeft,
                            targetContent,
                            overscrollCompletable = overscrollCompletable,
                        )
                        .toVelocity()
                },
            )
        } finally {
            overscrollCompletable.complete(Unit)
        }

        return velocity
        return swipeAnimation.animateOffset(velocity, targetContent, awaitFling = awaitFling)
    }

    /**
@@ -408,10 +413,6 @@ private class DragControllerImpl(
                isCloserToTarget()
        }
    }

    override fun onCancel() {
        swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f) }
    }
}

/** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
@@ -453,15 +454,15 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol
                    (actionSwipe.fromSource != null &&
                        actionSwipe.fromSource != swipe.fromSource) ||
                    // The action requires a specific pointerType.
                    (actionSwipe.pointersType != null &&
                        actionSwipe.pointersType != swipe.pointersType)
                    (actionSwipe.pointerType != null &&
                        actionSwipe.pointerType != swipe.pointerType)
            ) {
                // This action is not eligible.
                return@forEach
            }

            val sameFromSource = actionSwipe.fromSource == swipe.fromSource
            val samePointerType = actionSwipe.pointersType == swipe.pointersType
            val samePointerType = actionSwipe.pointerType == swipe.pointerType
            // Prioritize actions with a perfect match.
            if (sameFromSource && samePointerType) {
                return actionResult
@@ -496,82 +497,6 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol
    }
}

internal class NestedScrollHandlerImpl(
    private val draggableHandler: DraggableHandlerImpl,
    private val pointersInfoOwner: PointersInfoOwner,
) {
    val connection: PriorityNestedScrollConnection = nestedScrollConnection()

    private fun nestedScrollConnection(): PriorityNestedScrollConnection {
        var lastPointersDown: PointersInfo.PointersDown? = null

        return PriorityNestedScrollConnection(
            orientation = draggableHandler.orientation,
            canStartPreScroll = { _, _, _ -> false },
            canStartPostScroll = { offsetAvailable, _, _ ->
                if (offsetAvailable == 0f) return@PriorityNestedScrollConnection false

                lastPointersDown =
                    when (val info = pointersInfoOwner.pointersInfo()) {
                        PointersInfo.MouseWheel -> {
                            // Do not support mouse wheel interactions
                            return@PriorityNestedScrollConnection false
                        }

                        is PointersInfo.PointersDown -> info
                        null -> null
                    }

                draggableHandler.layoutImpl
                    .contentForUserActions()
                    .shouldEnableSwipes(draggableHandler.orientation)
            },
            onStart = { firstScroll ->
                scrollController(
                    dragController =
                        draggableHandler.onDragStarted(
                            pointersDown = lastPointersDown,
                            overSlop = firstScroll,
                        ),
                    pointersInfoOwner = pointersInfoOwner,
                )
            },
        )
    }
}

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

            return dragController.onDrag(delta = deltaScroll)
        }

        override suspend fun OnStopScope.onStop(initialVelocity: Float): Float {
            return dragController.onStop(velocity = initialVelocity)
        }

        override fun onCancel() {
            dragController.onCancel()
        }

        /**
         * 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
    }
}

/**
 * The number of pixels below which there won't be a visible difference in the transition and from
 * which the animation can stop.
@@ -580,12 +505,8 @@ private fun scrollController(
// account instead.
internal const val OffsetVisibilityThreshold = 0.5f

private object NoOpDragController : DragController {
private object NoOpDragController : NestedDraggable.Controller {
    override fun onDrag(delta: Float) = 0f

    override suspend fun onStop(velocity: Float) = 0f

    override fun onCancel() {
        /* do nothing */
    }
    override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float = 0f
}
+0 −678

File deleted.

Preview size limit exceeded, changes collapsed.

+18 −16
Original line number Diff line number Diff line
@@ -457,7 +457,7 @@ data class Swipe
private constructor(
    val direction: SwipeDirection,
    val pointerCount: Int = 1,
    val pointersType: PointerType? = null,
    val pointerType: PointerType? = null,
    val fromSource: SwipeSource? = null,
) : UserAction() {
    companion object {
@@ -470,46 +470,46 @@ private constructor(

        fun Left(
            pointerCount: Int = 1,
            pointersType: PointerType? = null,
            pointerType: PointerType? = null,
            fromSource: SwipeSource? = null,
        ) = Swipe(SwipeDirection.Left, pointerCount, pointersType, fromSource)
        ) = Swipe(SwipeDirection.Left, pointerCount, pointerType, fromSource)

        fun Up(
            pointerCount: Int = 1,
            pointersType: PointerType? = null,
            pointerType: PointerType? = null,
            fromSource: SwipeSource? = null,
        ) = Swipe(SwipeDirection.Up, pointerCount, pointersType, fromSource)
        ) = Swipe(SwipeDirection.Up, pointerCount, pointerType, fromSource)

        fun Right(
            pointerCount: Int = 1,
            pointersType: PointerType? = null,
            pointerType: PointerType? = null,
            fromSource: SwipeSource? = null,
        ) = Swipe(SwipeDirection.Right, pointerCount, pointersType, fromSource)
        ) = Swipe(SwipeDirection.Right, pointerCount, pointerType, fromSource)

        fun Down(
            pointerCount: Int = 1,
            pointersType: PointerType? = null,
            pointerType: PointerType? = null,
            fromSource: SwipeSource? = null,
        ) = Swipe(SwipeDirection.Down, pointerCount, pointersType, fromSource)
        ) = Swipe(SwipeDirection.Down, pointerCount, pointerType, fromSource)

        fun Start(
            pointerCount: Int = 1,
            pointersType: PointerType? = null,
            pointerType: PointerType? = null,
            fromSource: SwipeSource? = null,
        ) = Swipe(SwipeDirection.Start, pointerCount, pointersType, fromSource)
        ) = Swipe(SwipeDirection.Start, pointerCount, pointerType, fromSource)

        fun End(
            pointerCount: Int = 1,
            pointersType: PointerType? = null,
            pointerType: PointerType? = null,
            fromSource: SwipeSource? = null,
        ) = Swipe(SwipeDirection.End, pointerCount, pointersType, fromSource)
        ) = Swipe(SwipeDirection.End, pointerCount, pointerType, fromSource)
    }

    override fun resolve(layoutDirection: LayoutDirection): UserAction.Resolved {
        return Resolved(
            direction = direction.resolve(layoutDirection),
            pointerCount = pointerCount,
            pointersType = pointersType,
            pointerType = pointerType,
            fromSource = fromSource?.resolve(layoutDirection),
        )
    }
@@ -519,7 +519,7 @@ private constructor(
        val direction: SwipeDirection.Resolved,
        val pointerCount: Int,
        val fromSource: SwipeSource.Resolved?,
        val pointersType: PointerType?,
        val pointerType: PointerType?,
    ) : UserAction.Resolved()
}

@@ -724,6 +724,7 @@ internal fun SceneTransitionLayoutForTesting(
                density = density,
                layoutDirection = layoutDirection,
                swipeSourceDetector = swipeSourceDetector,
                swipeDetector = swipeDetector,
                transitionInterceptionThreshold = transitionInterceptionThreshold,
                builder = builder,
                animationScope = animationScope,
@@ -767,8 +768,9 @@ internal fun SceneTransitionLayoutForTesting(
        layoutImpl.density = density
        layoutImpl.layoutDirection = layoutDirection
        layoutImpl.swipeSourceDetector = swipeSourceDetector
        layoutImpl.swipeDetector = swipeDetector
        layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
    }

    layoutImpl.Content(modifier, swipeDetector)
    layoutImpl.Content(modifier)
}
+29 −41

File changed.

Preview size limit exceeded, changes collapsed.

+24 −34

File changed.

Preview size limit exceeded, changes collapsed.

Loading