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

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

Merge changes from topic "stl_overscroll_api" into main

* changes:
  OverscrollSpec could be used from TransitionState.Idle state
  DSL to define the overscroll behavior of a scene (1/2)
parents 17a2b99a 0b1d06bd
Loading
Loading
Loading
Loading
+57 −9
Original line number Diff line number Diff line
@@ -39,7 +39,6 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
@@ -204,6 +203,17 @@ internal class ElementNode(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        val overscrollScene = layoutImpl.state.currentOverscrollSpec?.scene
        if (overscrollScene != null && overscrollScene != scene.key) {
            // There is an overscroll in progress on another scene
            // By measuring composable elements, Compose can cache relevant information.
            // This reduces the need for re-measure when users return from an overscroll animation.
            val placeable = measurable.measure(constraints)
            return layout(placeable.width, placeable.height) {
                // We don't want to draw it, no need to place the element.
            }
        }

        val placeable = measure(layoutImpl, scene, element, sceneState, measurable, constraints)
        return layout(placeable.width, placeable.height) {
            place(layoutImpl, scene, element, sceneState, placeable, placementScope = this)
@@ -253,11 +263,13 @@ private fun shouldDrawElement(
): Boolean {
    val transition = layoutImpl.state.currentTransition

    // Always draw the element if there is no ongoing transition or if the element is not shared.
    // Always draw the element if there is no ongoing transition or if the element is not shared or
    // if the current scene is the one that is currently over scrolling with [OverscrollSpec].
    if (
        transition == null ||
            transition.fromScene !in element.sceneStates ||
            transition.toScene !in element.sceneStates
            transition.toScene !in element.sceneStates ||
            layoutImpl.state.currentOverscrollSpec?.scene == scene.key
    ) {
        return true
    }
@@ -286,12 +298,14 @@ internal fun shouldDrawOrComposeSharedElement(
    val fromScene = transition.fromScene
    val toScene = transition.toScene

    return scenePicker.sceneDuringTransition(
    val chosenByPicker =
        scenePicker.sceneDuringTransition(
            element = element,
            transition = transition,
            fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex,
            toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
        ) == scene
    return chosenByPicker || layoutImpl.state.currentOverscrollSpec?.scene == scene
}

private fun isSharedElementEnabled(
@@ -549,6 +563,40 @@ private inline fun <T> computeValue(
        return idleValue
    }

    if (transition is TransitionState.HasOverscrollProperties) {
        val overscroll = layoutImpl.state.currentOverscrollSpec
        if (overscroll?.scene == scene.key) {
            val elementSpec = overscroll.transformationSpec.transformations(element.key, scene.key)
            val propertySpec = transformation(elementSpec) ?: return currentValue()
            val overscrollState = checkNotNull(if (scene.key == toScene) toState else fromState)
            val targetValue =
                propertySpec.transform(
                    layoutImpl,
                    scene,
                    element,
                    overscrollState,
                    transition,
                    idleValue,
                )

            // Make sure we don't read progress if values are the same and we don't need to
            // interpolate, so we don't invalidate the phase where this is read.
            if (targetValue == idleValue) {
                return targetValue
            }

            // TODO(b/290184746): Make sure that we don't overflow transformations associated to a
            // range.
            val directionSign = if (transition.isUpOrLeft) -1 else 1
            val overscrollProgress = transition.progress.let { if (it > 1f) it - 1f else it }
            val progress = directionSign * overscrollProgress
            val rangeProgress = propertySpec.range?.progress(progress) ?: progress

            // Interpolate between the value at rest and the over scrolled value.
            return lerp(idleValue, targetValue, rangeProgress)
        }
    }

    // The element is shared: interpolate between the value in fromScene and the value in toScene.
    // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
    // elements follow the finger direction.
+14 −5
Original line number Diff line number Diff line
@@ -497,9 +497,11 @@ private class SwipeTransition(
    val _fromScene: Scene,
    val _toScene: Scene,
    private val userActionDistanceScope: UserActionDistanceScope,
    private val orientation: Orientation,
    private val isUpOrLeft: Boolean,
) : TransitionState.Transition(_fromScene.key, _toScene.key) {
    override val orientation: Orientation,
    override val isUpOrLeft: Boolean,
) :
    TransitionState.Transition(_fromScene.key, _toScene.key),
    TransitionState.HasOverscrollProperties {
    var _currentScene by mutableStateOf(_fromScene)
    override val currentScene: SceneKey
        get() = _currentScene.key
@@ -789,14 +791,21 @@ internal class SceneNestedScrollHandler(
            )

        fun hasNextScene(amount: Float): Boolean {
            val fromScene = layoutImpl.scene(layoutState.transitionState.currentScene)
            val transitionState = layoutState.transitionState
            val scene = transitionState.currentScene
            val fromScene = layoutImpl.scene(scene)
            val nextScene =
                when {
                    amount < 0f -> fromScene.userActions[actionUpOrLeft]
                    amount > 0f -> fromScene.userActions[actionDownOrRight]
                    else -> null
                }
            return nextScene != null
            if (nextScene != null) return true

            if (transitionState !is TransitionState.Idle) return false

            val overscrollSpec = layoutImpl.state.transitions.overscrollSpec(scene, orientation)
            return overscrollSpec != null
        }

        val source = this
+43 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.compose.animation.scene

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
@@ -221,6 +222,23 @@ sealed interface TransitionState {
                isTransitioning(from = other, to = scene)
        }
    }

    interface HasOverscrollProperties {
        /**
         * The position of the [TransitionState.Transition.toScene].
         *
         * Used to understand the direction of the overscroll.
         */
        val isUpOrLeft: Boolean

        /**
         * The relative orientation between [TransitionState.Transition.fromScene] and
         * [TransitionState.Transition.toScene].
         *
         * Used to understand the orientation of the overscroll.
         */
        val orientation: Orientation
    }
}

internal abstract class BaseSceneTransitionLayoutState(
@@ -237,6 +255,25 @@ internal abstract class BaseSceneTransitionLayoutState(
     */
    internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty

    private var fromOverscrollSpec: OverscrollSpecImpl? = null
    private var toOverscrollSpec: OverscrollSpecImpl? = null

    /**
     * @return the overscroll [OverscrollSpecImpl] if it is defined for the current
     *   [transitionState] and we are currently over scrolling.
     */
    internal val currentOverscrollSpec: OverscrollSpecImpl?
        get() {
            val transition = currentTransition ?: return null
            if (transition !is TransitionState.HasOverscrollProperties) return null
            val progress = transition.progress
            return when {
                progress < 0f -> fromOverscrollSpec
                progress > 1f -> toOverscrollSpec
                else -> null
            }
        }

    private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()

    /** Whether we can transition to the given [scene]. */
@@ -266,10 +303,13 @@ internal abstract class BaseSceneTransitionLayoutState(
        transitionKey: TransitionKey?,
    ) {
        // Compute the [TransformationSpec] when the transition starts.
        val fromScene = transition.fromScene
        val toScene = transition.toScene
        val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
        transformationSpec =
            transitions
                .transitionSpec(transition.fromScene, transition.toScene, key = transitionKey)
                .transformationSpec()
            transitions.transitionSpec(fromScene, toScene, key = transitionKey).transformationSpec()
        fromOverscrollSpec = orientation?.let { transitions.overscrollSpec(fromScene, it) }
        toOverscrollSpec = orientation?.let { transitions.overscrollSpec(toScene, it) }
        cancelActiveTransitionLinks()
        setupTransitionLinks(transition)
        transitionState = transition
+54 −4
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
@@ -41,18 +42,22 @@ class SceneTransitions
internal constructor(
    internal val defaultSwipeSpec: SpringSpec<Float>,
    internal val transitionSpecs: List<TransitionSpecImpl>,
    internal val overscrollSpecs: List<OverscrollSpecImpl>,
) {
    private val cache =
    private val transitionCache =
        mutableMapOf<
            SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>>
        >()

    private val overscrollCache =
        mutableMapOf<SceneKey, MutableMap<Orientation, OverscrollSpecImpl?>>()

    internal fun transitionSpec(
        from: SceneKey,
        to: SceneKey,
        key: TransitionKey?,
    ): TransitionSpecImpl {
        return cache
        return transitionCache
            .getOrPut(from) { mutableMapOf() }
            .getOrPut(to) { mutableMapOf() }
            .getOrPut(key) { findSpec(from, to, key) }
@@ -105,6 +110,28 @@ internal constructor(
    private fun defaultTransition(from: SceneKey, to: SceneKey) =
        TransitionSpecImpl(key = null, from, to, TransformationSpec.EmptyProvider)

    internal fun overscrollSpec(scene: SceneKey, orientation: Orientation): OverscrollSpecImpl? =
        overscrollCache
            .getOrPut(scene) { mutableMapOf() }
            .getOrPut(orientation) { overscroll(scene, orientation) { it.scene == scene } }

    private fun overscroll(
        scene: SceneKey,
        orientation: Orientation,
        filter: (OverscrollSpecImpl) -> Boolean,
    ): OverscrollSpecImpl? {
        var match: OverscrollSpecImpl? = null
        overscrollSpecs.fastForEach { spec ->
            if (spec.orientation == orientation && filter(spec)) {
                if (match != null) {
                    error("Found multiple transition specs for transition $scene")
                }
                match = spec
            }
        }
        return match
    }

    companion object {
        internal val DefaultSwipeSpec =
            spring(
@@ -112,7 +139,12 @@ internal constructor(
                visibilityThreshold = OffsetVisibilityThreshold,
            )

        val Empty = SceneTransitions(DefaultSwipeSpec, transitionSpecs = emptyList())
        val Empty =
            SceneTransitions(
                defaultSwipeSpec = DefaultSwipeSpec,
                transitionSpecs = emptyList(),
                overscrollSpecs = emptyList(),
            )
    }
}

@@ -139,7 +171,7 @@ interface TransitionSpec {
     */
    fun reversed(): TransitionSpec

    /*
    /**
     * The [TransformationSpec] associated to this [TransitionSpec].
     *
     * Note that this is called once every a transition associated to this [TransitionSpec] is
@@ -212,6 +244,24 @@ internal class TransitionSpecImpl(
    override fun transformationSpec(): TransformationSpecImpl = this.transformationSpec.invoke()
}

/** The definition of the overscroll behavior of the [scene]. */
interface OverscrollSpec {
    /** The scene we are over scrolling. */
    val scene: SceneKey

    /** The orientation of this [OverscrollSpec]. */
    val orientation: Orientation

    /** The [TransformationSpec] associated to this [OverscrollSpec]. */
    val transformationSpec: TransformationSpec
}

internal class OverscrollSpecImpl(
    override val scene: SceneKey,
    override val orientation: Orientation,
    override val transformationSpec: TransformationSpecImpl,
) : OverscrollSpec

/**
 * An implementation of [TransformationSpec] that allows the quick retrieval of an element
 * [ElementTransformations].
+30 −13
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.compose.animation.scene

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -72,24 +73,23 @@ interface SceneTransitionsBuilder {
        key: TransitionKey? = null,
        builder: TransitionBuilder.() -> Unit = {},
    ): TransitionSpec
}

@TransitionDsl
interface TransitionBuilder : PropertyTransformationBuilder {
    /**
     * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
     * the transition is triggered (i.e. it is not gesture-based).
     */
    var spec: AnimationSpec<Float>

    /**
     * The [SpringSpec] used to animate the associated transition progress when the transition was
     * started by a swipe and is now animating back to a scene because the user lifted their finger.
     * Define the animation to be played when the [scene] is overscrolled in the given
     * [orientation].
     *
     * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
     * The overscroll animation always starts from a progress of 0f, and reaches 1f when moving the
     * [distance] down/right, -1f when moving in the opposite direction.
     */
    var swipeSpec: SpringSpec<Float>?
    fun overscroll(
        scene: SceneKey,
        orientation: Orientation,
        builder: OverscrollBuilder.() -> Unit = {},
    ): OverscrollSpec
}

@TransitionDsl
interface OverscrollBuilder : PropertyTransformationBuilder {
    /**
     * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
     * [UserAction].
@@ -117,6 +117,23 @@ interface TransitionBuilder : PropertyTransformationBuilder {
        end: Float? = null,
        builder: PropertyTransformationBuilder.() -> Unit,
    )
}

@TransitionDsl
interface TransitionBuilder : OverscrollBuilder, PropertyTransformationBuilder {
    /**
     * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
     * the transition is triggered (i.e. it is not gesture-based).
     */
    var spec: AnimationSpec<Float>

    /**
     * The [SpringSpec] used to animate the associated transition progress when the transition was
     * started by a swipe and is now animating back to a scene because the user lifted their finger.
     *
     * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
     */
    var swipeSpec: SpringSpec<Float>?

    /**
     * Define a timestamp-based range for the transformations inside [builder].
Loading