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

Commit e201ccf8 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge changes I149a3382,I160b4a75,Ibaa2dfc9,I65511c83 into main

* changes:
  Move more swipe animation logic into SwipeAnimation
  Move SwipeAnimation into its own file
  Add support for user actions in overlays
  Extract SwipeAnimation out of SwipeTransition
parents 92887fca c54d612c
Loading
Loading
Loading
Loading
+172 −522

File changed.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -269,13 +269,13 @@ internal class MultiPointerDraggableNode(
                                                    velocityTracker.calculateVelocity(maxVelocity)
                                                }
                                                .toFloat(),
                                        onFling = { controller.onStop(it, canChangeScene = true) }
                                        onFling = { controller.onStop(it, canChangeContent = true) }
                                    )
                                },
                                onDragCancel = { controller ->
                                    startFlingGesture(
                                        initialVelocity = 0f,
                                        onFling = { controller.onStop(it, canChangeScene = true) }
                                        onFling = { controller.onStop(it, canChangeContent = true) }
                                    )
                                },
                                swipeDetector = swipeDetector,
+68 −9
Original line number Diff line number Diff line
@@ -104,10 +104,10 @@ interface SceneTransitionLayoutScope {
     * call order. Calling overlay(A) followed by overlay(B) will mean that overlay B renders
     * after/above overlay A.
     */
    // TODO(b/353679003): Allow to specify user actions. When overlays are shown, the user actions
    // of the top-most overlay in currentOverlays will be used.
    fun overlay(
        key: OverlayKey,
        userActions: Map<UserAction, UserActionResult> =
            mapOf(Back to UserActionResult.HideOverlay(key)),
        alignment: Alignment = Alignment.Center,
        content: @Composable ContentScope.() -> Unit,
    )
@@ -479,20 +479,79 @@ interface SwipeSourceDetector {
}

/** The result of performing a [UserAction]. */
data class UserActionResult(
    /** The scene we should be transitioning to during the [UserAction]. */
    val toScene: SceneKey,

sealed class UserActionResult(
    /** The key of the transition that should be used. */
    val transitionKey: TransitionKey? = null,
    open val transitionKey: TransitionKey? = null,

    /**
     * If `true`, the swipe will be committed and we will settle to [toScene] if only if the user
     * swiped at least the swipe distance, i.e. the transition progress was already equal to or
     * bigger than 100% when the user released their finger. `
     */
    val requiresFullDistanceSwipe: Boolean = false,
)
    open val requiresFullDistanceSwipe: Boolean,
) {
    internal abstract fun toContent(currentScene: SceneKey): ContentKey

    data class ChangeScene
    internal constructor(
        /** The scene we should be transitioning to during the [UserAction]. */
        val toScene: SceneKey,
        override val transitionKey: TransitionKey? = null,
        override val requiresFullDistanceSwipe: Boolean = false,
    ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
        override fun toContent(currentScene: SceneKey): ContentKey = toScene
    }

    /** A [UserActionResult] that shows [overlay]. */
    class ShowOverlay(
        val overlay: OverlayKey,
        transitionKey: TransitionKey? = null,
        requiresFullDistanceSwipe: Boolean = false,
    ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
        override fun toContent(currentScene: SceneKey): ContentKey = overlay
    }

    /** A [UserActionResult] that hides [overlay]. */
    class HideOverlay(
        val overlay: OverlayKey,
        transitionKey: TransitionKey? = null,
        requiresFullDistanceSwipe: Boolean = false,
    ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
        override fun toContent(currentScene: SceneKey): ContentKey = currentScene
    }

    /**
     * A [UserActionResult] that replaces the current overlay by [overlay].
     *
     * Note: This result can only be used for user actions of overlays and an exception will be
     * thrown if it is used for a scene.
     */
    class ReplaceByOverlay(
        val overlay: OverlayKey,
        transitionKey: TransitionKey? = null,
        requiresFullDistanceSwipe: Boolean = false,
    ) : UserActionResult(transitionKey, requiresFullDistanceSwipe) {
        override fun toContent(currentScene: SceneKey): ContentKey = overlay
    }

    companion object {
        /** A [UserActionResult] that changes the current scene to [toScene]. */
        operator fun invoke(
            /** The scene we should be transitioning to during the [UserAction]. */
            toScene: SceneKey,

            /** The key of the transition that should be used. */
            transitionKey: TransitionKey? = null,

            /**
             * If `true`, the swipe will be committed if only if the user swiped at least the swipe
             * distance, i.e. the transition progress was already equal to or bigger than 100% when
             * the user released their finger.
             */
            requiresFullDistanceSwipe: Boolean = false,
        ): UserActionResult = ChangeScene(toScene, transitionKey, requiresFullDistanceSwipe)
    }
}

fun interface UserActionDistance {
    /**
+91 −8
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.compose.animation.scene

import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -31,6 +32,7 @@ import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.node.LayoutAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -132,6 +134,8 @@ internal class SceneTransitionLayoutImpl(
    internal lateinit var lookaheadScope: LookaheadScope
        private set

    internal var lastSize: IntSize = IntSize.Zero

    init {
        updateContents(builder, layoutDirection)

@@ -179,6 +183,28 @@ internal class SceneTransitionLayoutImpl(
        }
    }

    internal fun contentForUserActions(): Content {
        return findOverlayWithHighestZIndex() ?: scene(state.transitionState.currentScene)
    }

    private fun findOverlayWithHighestZIndex(): Overlay? {
        val currentOverlays = state.transitionState.currentOverlays
        if (currentOverlays.isEmpty()) {
            return null
        }

        var overlay: Overlay? = null
        currentOverlays.forEach { key ->
            val previousZIndex = overlay?.zIndex
            val candidate = overlay(key)
            if (previousZIndex == null || candidate.zIndex > previousZIndex) {
                overlay = candidate
            }
        }

        return overlay
    }

    internal fun updateContents(
        builder: SceneTransitionLayoutScope.() -> Unit,
        layoutDirection: LayoutDirection,
@@ -203,8 +229,7 @@ internal class SceneTransitionLayoutImpl(

                    scenesToRemove.remove(key)

                    val resolvedUserActions =
                        userActions.mapKeys { it.key.resolve(layoutDirection) }
                    val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection)
                    val scene = scenes[key]
                    if (scene != null) {
                        // Update an existing scene.
@@ -228,6 +253,7 @@ internal class SceneTransitionLayoutImpl(

                override fun overlay(
                    key: OverlayKey,
                    userActions: Map<UserAction, UserActionResult>,
                    alignment: Alignment,
                    content: @Composable (ContentScope.() -> Unit)
                ) {
@@ -235,10 +261,12 @@ internal class SceneTransitionLayoutImpl(
                    overlaysToRemove.remove(key)

                    val overlay = overlays[key]
                    val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection)
                    if (overlay != null) {
                        // Update an existing overlay.
                        overlay.content = content
                        overlay.zIndex = zIndex
                        overlay.userActions = resolvedUserActions
                        overlay.alignment = alignment
                    } else {
                        // New overlay.
@@ -247,8 +275,7 @@ internal class SceneTransitionLayoutImpl(
                                key,
                                this@SceneTransitionLayoutImpl,
                                content,
                                // TODO(b/353679003): Allow to specify user actions
                                actions = emptyMap(),
                                resolvedUserActions,
                                zIndex,
                                alignment,
                            )
@@ -263,6 +290,46 @@ internal class SceneTransitionLayoutImpl(
        overlaysToRemove.forEach { overlays.remove(it) }
    }

    private fun resolveUserActions(
        key: ContentKey,
        userActions: Map<UserAction, UserActionResult>,
        layoutDirection: LayoutDirection
    ): Map<UserAction.Resolved, UserActionResult> {
        return userActions
            .mapKeys { it.key.resolve(layoutDirection) }
            .also { checkUserActions(key, it) }
    }

    private fun checkUserActions(
        key: ContentKey,
        userActions: Map<UserAction.Resolved, UserActionResult>,
    ) {
        userActions.forEach { (action, result) ->
            fun details() = "Content $key, action $action, result $result."

            when (result) {
                is UserActionResult.ChangeScene -> {
                    check(key != result.toScene) {
                        error("Transition to the same scene is not supported. ${details()}")
                    }
                }
                is UserActionResult.ReplaceByOverlay -> {
                    check(key is OverlayKey) {
                        "ReplaceByOverlay() can only be used for overlays, not scenes. ${details()}"
                    }

                    check(key != result.overlay) {
                        "Transition to the same overlay is not supported. ${details()}"
                    }
                }
                is UserActionResult.ShowOverlay,
                is UserActionResult.HideOverlay -> {
                    /* Always valid. */
                }
            }
        }
    }

    @Composable
    internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) {
        Box(
@@ -287,7 +354,17 @@ internal class SceneTransitionLayoutImpl(
    @Composable
    private fun BackHandler() {
        val targetSceneForBack =
            scene(state.transitionState.currentScene).userActions[Back.Resolved]?.toScene
            when (val result = contentForUserActions().userActions[Back.Resolved]) {
                null -> null
                is UserActionResult.ChangeScene -> result.toScene
                is UserActionResult.ShowOverlay,
                is UserActionResult.HideOverlay,
                is UserActionResult.ReplaceByOverlay -> {
                    // TODO(b/353679003): Support overlay transitions when going back
                    null
                }
            }

        PredictiveBackHandler(state, coroutineScope, targetSceneForBack)
    }

@@ -379,8 +456,10 @@ internal class SceneTransitionLayoutImpl(
            .sortedBy { it.zIndex }
    }

    internal fun setScenesTargetSizeForTest(size: IntSize) {
        scenes.values.forEach { it.targetSize = size }
    @VisibleForTesting
    internal fun setContentsAndLayoutTargetSizeForTest(size: IntSize) {
        lastSize = size
        (scenes.values + overlays.values).forEach { it.targetSize = size }
    }

    internal fun overlaysOrNullForTest(): Map<OverlayKey, Overlay>? = _overlays
@@ -396,7 +475,11 @@ private data class LayoutElement(private val layoutImpl: SceneTransitionLayoutIm
}

private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) :
    Modifier.Node(), ApproachLayoutModifierNode {
    Modifier.Node(), ApproachLayoutModifierNode, LayoutAwareModifierNode {
    override fun onRemeasured(size: IntSize) {
        layoutImpl.lastSize = size
    }

    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
        return layoutImpl.state.isTransitioning()
    }
+17 −0
Original line number Diff line number Diff line
@@ -183,6 +183,12 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState
 *   commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns
 *   `true`, then the gesture will be committed and we will animate to the other scene. Otherwise,
 *   the gesture will be cancelled and we will animate back to the current scene.
 * @param canShowOverlay whether we should commit a user action that will result in showing the
 *   given overlay.
 * @param canHideOverlay whether we should commit a user action that will result in hiding the given
 *   overlay.
 * @param canReplaceOverlay whether we should commit a user action that will result in replacing
 *   `from` overlay by `to` overlay.
 * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other
 *   [SceneTransitionLayoutState]s.
 */
@@ -191,6 +197,9 @@ fun MutableSceneTransitionLayoutState(
    transitions: SceneTransitions = SceneTransitions.Empty,
    initialOverlays: Set<OverlayKey> = emptySet(),
    canChangeScene: (SceneKey) -> Boolean = { true },
    canShowOverlay: (OverlayKey) -> Boolean = { true },
    canHideOverlay: (OverlayKey) -> Boolean = { true },
    canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
    stateLinks: List<StateLink> = emptyList(),
    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
): MutableSceneTransitionLayoutState {
@@ -199,6 +208,9 @@ fun MutableSceneTransitionLayoutState(
        transitions,
        initialOverlays,
        canChangeScene,
        canShowOverlay,
        canHideOverlay,
        canReplaceOverlay,
        stateLinks,
        enableInterruptions,
    )
@@ -210,6 +222,11 @@ internal class MutableSceneTransitionLayoutStateImpl(
    override var transitions: SceneTransitions = transitions {},
    initialOverlays: Set<OverlayKey> = emptySet(),
    internal val canChangeScene: (SceneKey) -> Boolean = { true },
    internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
    internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
        true
    },
    private val stateLinks: List<StateLink> = emptyList(),

    // TODO(b/290930950): Remove this flag.
Loading