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

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

Merge changes Ie2944544,Ia7cec977,Id4e5ff33,Iad082ca5 into main

* changes:
  Add support for interruptions in shared values
  Move the implementation of AnimatedState into AnimatedStateImpl
  Make animated values support multiple transitions
  Expose the current scene key in SceneScope
parents 144dc689 27f7b95d
Loading
Loading
Loading
Loading
+257 −89
Original line number Diff line number Diff line
@@ -27,11 +27,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.lerp
import androidx.compose.ui.util.fastLastOrNull
import kotlin.math.roundToInt

/**
 * A [State] whose [value] is animated.
@@ -74,7 +75,7 @@ fun SceneScope.animateSceneIntAsState(
    key: ValueKey,
    canOverflow: Boolean = true,
): AnimatedState<Int> {
    return animateSceneValueAsState(value, key, ::lerp, canOverflow)
    return animateSceneValueAsState(value, key, SharedIntType, canOverflow)
}

/**
@@ -88,7 +89,19 @@ fun ElementScope<*>.animateElementIntAsState(
    key: ValueKey,
    canOverflow: Boolean = true,
): AnimatedState<Int> {
    return animateElementValueAsState(value, key, ::lerp, canOverflow)
    return animateElementValueAsState(value, key, SharedIntType, canOverflow)
}

private object SharedIntType : SharedValueType<Int, Int> {
    override val unspecifiedValue: Int = Int.MIN_VALUE
    override val zeroDeltaValue: Int = 0

    override fun lerp(a: Int, b: Int, progress: Float): Int =
        androidx.compose.ui.util.lerp(a, b, progress)

    override fun diff(a: Int, b: Int): Int = a - b

    override fun addWeighted(a: Int, b: Int, bWeight: Float): Int = (a + b * bWeight).roundToInt()
}

/**
@@ -102,7 +115,7 @@ fun SceneScope.animateSceneFloatAsState(
    key: ValueKey,
    canOverflow: Boolean = true,
): AnimatedState<Float> {
    return animateSceneValueAsState(value, key, ::lerp, canOverflow)
    return animateSceneValueAsState(value, key, SharedFloatType, canOverflow)
}

/**
@@ -116,7 +129,19 @@ fun ElementScope<*>.animateElementFloatAsState(
    key: ValueKey,
    canOverflow: Boolean = true,
): AnimatedState<Float> {
    return animateElementValueAsState(value, key, ::lerp, canOverflow)
    return animateElementValueAsState(value, key, SharedFloatType, canOverflow)
}

private object SharedFloatType : SharedValueType<Float, Float> {
    override val unspecifiedValue: Float = Float.MIN_VALUE
    override val zeroDeltaValue: Float = 0f

    override fun lerp(a: Float, b: Float, progress: Float): Float =
        androidx.compose.ui.util.lerp(a, b, progress)

    override fun diff(a: Float, b: Float): Float = a - b

    override fun addWeighted(a: Float, b: Float, bWeight: Float): Float = a + b * bWeight
}

/**
@@ -130,7 +155,7 @@ fun SceneScope.animateSceneDpAsState(
    key: ValueKey,
    canOverflow: Boolean = true,
): AnimatedState<Dp> {
    return animateSceneValueAsState(value, key, ::lerp, canOverflow)
    return animateSceneValueAsState(value, key, SharedDpType, canOverflow)
}

/**
@@ -144,7 +169,20 @@ fun ElementScope<*>.animateElementDpAsState(
    key: ValueKey,
    canOverflow: Boolean = true,
): AnimatedState<Dp> {
    return animateElementValueAsState(value, key, ::lerp, canOverflow)
    return animateElementValueAsState(value, key, SharedDpType, canOverflow)
}

private object SharedDpType : SharedValueType<Dp, Dp> {
    override val unspecifiedValue: Dp = Dp.Unspecified
    override val zeroDeltaValue: Dp = 0.dp

    override fun lerp(a: Dp, b: Dp, progress: Float): Dp {
        return androidx.compose.ui.unit.lerp(a, b, progress)
    }

    override fun diff(a: Dp, b: Dp): Dp = a - b

    override fun addWeighted(a: Dp, b: Dp, bWeight: Float): Dp = a + b * bWeight
}

/**
@@ -157,7 +195,7 @@ fun SceneScope.animateSceneColorAsState(
    value: Color,
    key: ValueKey,
): AnimatedState<Color> {
    return animateSceneValueAsState(value, key, ::lerp, canOverflow = false)
    return animateSceneValueAsState(value, key, SharedColorType, canOverflow = false)
}

/**
@@ -170,9 +208,56 @@ fun ElementScope<*>.animateElementColorAsState(
    value: Color,
    key: ValueKey,
): AnimatedState<Color> {
    return animateElementValueAsState(value, key, ::lerp, canOverflow = false)
    return animateElementValueAsState(value, key, SharedColorType, canOverflow = false)
}

private object SharedColorType : SharedValueType<Color, ColorDelta> {
    override val unspecifiedValue: Color = Color.Unspecified
    override val zeroDeltaValue: ColorDelta = ColorDelta(0f, 0f, 0f, 0f)

    override fun lerp(a: Color, b: Color, progress: Float): Color {
        return androidx.compose.ui.graphics.lerp(a, b, progress)
    }

    override fun diff(a: Color, b: Color): ColorDelta {
        // Similar to lerp, we convert colors to the Oklab color space to perform operations on
        // colors.
        val aOklab = a.convert(ColorSpaces.Oklab)
        val bOklab = b.convert(ColorSpaces.Oklab)
        return ColorDelta(
            red = aOklab.red - bOklab.red,
            green = aOklab.green - bOklab.green,
            blue = aOklab.blue - bOklab.blue,
            alpha = aOklab.alpha - bOklab.alpha,
        )
    }

    override fun addWeighted(a: Color, b: ColorDelta, bWeight: Float): Color {
        val aOklab = a.convert(ColorSpaces.Oklab)
        return Color(
                red = aOklab.red + b.red * bWeight,
                green = aOklab.green + b.green * bWeight,
                blue = aOklab.blue + b.blue * bWeight,
                alpha = aOklab.alpha + b.alpha * bWeight,
                colorSpace = ColorSpaces.Oklab,
            )
            .convert(aOklab.colorSpace)
    }
}

/**
 * Represents the diff between two colors in the same color space.
 *
 * Note: This class is necessary because Color() checks the bounds of its values and UncheckedColor
 * is internal.
 */
private class ColorDelta(
    val red: Float,
    val green: Float,
    val blue: Float,
    val alpha: Float,
)

@Composable
internal fun <T> animateSharedValueAsState(
    layoutImpl: SceneTransitionLayoutImpl,
@@ -180,23 +265,22 @@ internal fun <T> animateSharedValueAsState(
    element: ElementKey?,
    key: ValueKey,
    value: T,
    lerp: (T, T, Float) -> T,
    type: SharedValueType<T, *>,
    canOverflow: Boolean,
): AnimatedState<T> {
    DisposableEffect(layoutImpl, scene, element, key) {
        // Create the associated maps that hold the current value for each (element, scene) pair.
        val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
        val sceneToValueMap =
            valueMap.getOrPut(element) { SnapshotStateMap<SceneKey, Any>() }
                as SnapshotStateMap<SceneKey, T>
        sceneToValueMap[scene] = value
        val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *>
        val targetValues = sharedValue.targetValues
        targetValues[scene] = value

        onDispose {
            // Remove the value associated to the current scene, and eventually remove the maps if
            // they are empty.
            sceneToValueMap.remove(scene)
            targetValues.remove(scene)

            if (sceneToValueMap.isEmpty() && valueMap[element] === sceneToValueMap) {
            if (targetValues.isEmpty() && valueMap[element] === sharedValue) {
                valueMap.remove(element)

                if (valueMap.isEmpty() && layoutImpl.sharedValues[key] === valueMap) {
@@ -208,34 +292,25 @@ internal fun <T> animateSharedValueAsState(

    // Update the current value. Note that side effects run after disposable effects, so we know
    // that the associated maps were created at this point.
    SideEffect { sceneToValueMap<T>(layoutImpl, key, element)[scene] = value }

    return remember(layoutImpl, scene, element, lerp, canOverflow) {
        object : AnimatedState<T> {
            override val value: T
                get() = value(layoutImpl, scene, element, key, lerp, canOverflow)

            @Composable
            override fun unsafeCompositionState(initialValue: T): State<T> {
                val state = remember { mutableStateOf(initialValue) }

                val animatedState = this
                LaunchedEffect(animatedState) {
                    snapshotFlow { animatedState.value }.collect { state.value = it }
    SideEffect {
        if (value == type.unspecifiedValue) {
            error("value is equal to $value, which is the undefined value for this type.")
        }

                return state
            }
        sharedValue<T, Any>(layoutImpl, key, element).targetValues[scene] = value
    }

    return remember(layoutImpl, scene, element, canOverflow) {
        AnimatedStateImpl<T, Any>(layoutImpl, scene, element, key, canOverflow)
    }
}

private fun <T> sceneToValueMap(
private fun <T, Delta> sharedValue(
    layoutImpl: SceneTransitionLayoutImpl,
    key: ValueKey,
    element: ElementKey?
): MutableMap<SceneKey, T> {
    return layoutImpl.sharedValues[key]?.get(element)?.let { it as SnapshotStateMap<SceneKey, T> }
): SharedValue<T, Delta> {
    return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> }
        ?: error(valueReadTooEarlyMessage(key))
}

@@ -244,62 +319,155 @@ private fun valueReadTooEarlyMessage(key: ValueKey) =
        "means that you are reading it during composition, which you should not do. See the " +
        "documentation of AnimatedState for more information."

private fun <T> value(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: SceneKey,
    element: ElementKey?,
    key: ValueKey,
    lerp: (T, T, Float) -> T,
    canOverflow: Boolean,
): T {
    return valueOrNull(layoutImpl, scene, element, key, lerp, canOverflow)
internal class SharedValue<T, Delta>(
    val type: SharedValueType<T, Delta>,
) {
    /** The target value of this shared value for each scene. */
    val targetValues = SnapshotStateMap<SceneKey, T>()

    /** The last value of this shared value. */
    var lastValue: T = type.unspecifiedValue

    /** The value of this shared value before the last interruption (if any). */
    var valueBeforeInterruption: T = type.unspecifiedValue

    /** The delta value to add to this shared value to have smoother interruptions. */
    var valueInterruptionDelta = type.zeroDeltaValue

    /** The last transition that was used when the value of this shared state. */
    var lastTransition: TransitionState.Transition? = null
}

private class AnimatedStateImpl<T, Delta>(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val scene: SceneKey,
    private val element: ElementKey?,
    private val key: ValueKey,
    private val canOverflow: Boolean,
) : AnimatedState<T> {
    override val value: T
        get() = value()

    private fun value(): T {
        val sharedValue = sharedValue<T, Delta>(layoutImpl, key, element)
        val transition = transition(sharedValue)
        val value: T =
            valueOrNull(sharedValue, transition)
                // TODO(b/311600838): Remove this. We should not have to fallback to the current
                // scene value, but we have to because code of removed nodes can still run if they
                // are placed with a graphics layer.
                ?: sharedValue[scene]
                ?: error(valueReadTooEarlyMessage(key))
        val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
        sharedValue.lastValue = interruptedValue
        return interruptedValue
    }

private fun <T> valueOrNull(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: SceneKey,
    element: ElementKey?,
    key: ValueKey,
    lerp: (T, T, Float) -> T,
    canOverflow: Boolean,
    private operator fun SharedValue<T, *>.get(scene: SceneKey): T? = targetValues[scene]

    private fun valueOrNull(
        sharedValue: SharedValue<T, *>,
        transition: TransitionState.Transition?,
    ): T? {
    val sceneToValueMap = sceneToValueMap<T>(layoutImpl, key, element)
    fun sceneValue(scene: SceneKey): T? = sceneToValueMap[scene]

    return when (val transition = layoutImpl.state.transitionState) {
        is TransitionState.Idle -> sceneValue(transition.currentScene)
        is TransitionState.Transition -> {
            // Note: no need to check for transition ready here given that all target values are
            // defined during composition, we should already have the correct values to interpolate
            // between here.
            val fromValue = sceneValue(transition.fromScene)
            val toValue = sceneValue(transition.toScene)
            if (fromValue != null && toValue != null) {
        if (transition == null) {
            return sharedValue[layoutImpl.state.transitionState.currentScene]
        }

        val fromValue = sharedValue[transition.fromScene]
        val toValue = sharedValue[transition.toScene]
        return if (fromValue != null && toValue != null) {
            if (fromValue == toValue) {
                // Optimization: avoid reading progress if the values are the same, so we don't
                // relayout/redraw for nothing.
                fromValue
            } else {
                    // In the case of bouncing, if the value remains constant during the overscroll,
                    // we should use the value of the scene we are bouncing around.
                // In the case of bouncing, if the value remains constant during the overscroll, we
                // should use the value of the scene we are bouncing around.
                if (!canOverflow && transition is TransitionState.HasOverscrollProperties) {
                    val bouncingScene = transition.bouncingScene
                    if (bouncingScene != null) {
                            return sceneValue(bouncingScene)
                        return sharedValue[bouncingScene]
                    }
                }

                val progress =
                    if (canOverflow) transition.progress
                    else transition.progress.fastCoerceIn(0f, 1f)
                    lerp(fromValue, toValue, progress)
                sharedValue.type.lerp(fromValue, toValue, progress)
            }
        } else fromValue ?: toValue
    }

    private fun transition(sharedValue: SharedValue<T, Delta>): TransitionState.Transition? {
        val targetValues = sharedValue.targetValues
        val transition =
            if (element != null) {
                layoutImpl.elements[element]?.sceneStates?.let { sceneStates ->
                    layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
                        transition.fromScene in sceneStates || transition.toScene in sceneStates
                    }
                }
            } else {
                layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
                    transition.fromScene in targetValues || transition.toScene in targetValues
                }
            }

        val previousTransition = sharedValue.lastTransition
        sharedValue.lastTransition = transition

        if (transition != previousTransition && transition != null && previousTransition != null) {
            // The previous transition was interrupted by another transition.
            sharedValue.valueBeforeInterruption = sharedValue.lastValue
            sharedValue.valueInterruptionDelta = sharedValue.type.zeroDeltaValue
        } else if (transition == null && previousTransition != null) {
            // The transition was just finished.
            sharedValue.valueBeforeInterruption = sharedValue.type.unspecifiedValue
            sharedValue.valueInterruptionDelta = sharedValue.type.zeroDeltaValue
        }

        return transition
    }

    /**
     * Compute what [value] should be if we take the
     * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into
     * account.
     */
    private fun computeInterruptedValue(
        sharedValue: SharedValue<T, Delta>,
        transition: TransitionState.Transition?,
        value: T,
    ): T {
        val type = sharedValue.type
        if (sharedValue.valueBeforeInterruption != type.unspecifiedValue) {
            sharedValue.valueInterruptionDelta =
                type.diff(sharedValue.valueBeforeInterruption, value)
            sharedValue.valueBeforeInterruption = type.unspecifiedValue
        }

        val delta = sharedValue.valueInterruptionDelta
        return if (delta == type.zeroDeltaValue || transition == null) {
            value
        } else {
            val interruptionProgress = transition.interruptionProgress(layoutImpl)
            if (interruptionProgress == 0f) {
                value
            } else {
                type.addWeighted(value, delta, interruptionProgress)
            }
        }
    }

    @Composable
    override fun unsafeCompositionState(initialValue: T): State<T> {
        val state = remember { mutableStateOf(initialValue) }

        val animatedState = this
        LaunchedEffect(animatedState) {
            snapshotFlow { animatedState.value }.collect { state.value = it }
        }

        return state
    }
    // TODO(b/311600838): Remove this. We should not have to fallback to the current scene value,
    // but we have to because code of removed nodes can still run if they are placed with a graphics
    // layer.
    ?: sceneValue(scene)
}
+3 −4
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ private abstract class BaseElementScope<ContentScope>(
    override fun <T> animateElementValueAsState(
        value: T,
        key: ValueKey,
        lerp: (start: T, stop: T, fraction: Float) -> T,
        type: SharedValueType<T, *>,
        canOverflow: Boolean
    ): AnimatedState<T> {
        return animateSharedValueAsState(
@@ -86,7 +86,7 @@ private abstract class BaseElementScope<ContentScope>(
            element,
            key,
            value,
            lerp,
            type,
            canOverflow,
        )
    }
@@ -184,8 +184,7 @@ private fun shouldComposeMovableElement(
                fromSceneZIndex = layoutImpl.scenes.getValue(transition.fromScene).zIndex,
                toSceneZIndex = layoutImpl.scenes.getValue(transition.toScene).zIndex,
            ) != null
        }
            ?: return false
        } ?: return false

    // Always compose movable elements in the scene picked by their scene picker.
    return shouldDrawOrComposeSharedElement(
+3 −4
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.platform.testTag
@@ -69,7 +68,6 @@ internal class Scene(
    }

    @Composable
    @OptIn(ExperimentalComposeUiApi::class)
    fun Content(modifier: Modifier = Modifier) {
        Box(
            modifier
@@ -96,6 +94,7 @@ internal class SceneScopeImpl(
    private val layoutImpl: SceneTransitionLayoutImpl,
    private val scene: Scene,
) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
    override val sceneKey: SceneKey = scene.key
    override val layoutState: SceneTransitionLayoutState = layoutImpl.state

    override fun Modifier.element(key: ElementKey): Modifier {
@@ -124,7 +123,7 @@ internal class SceneScopeImpl(
    override fun <T> animateSceneValueAsState(
        value: T,
        key: ValueKey,
        lerp: (T, T, Float) -> T,
        type: SharedValueType<T, *>,
        canOverflow: Boolean
    ): AnimatedState<T> {
        return animateSharedValueAsState(
@@ -133,7 +132,7 @@ internal class SceneScopeImpl(
            element = null,
            key = key,
            value = value,
            lerp = lerp,
            type = type,
            canOverflow = canOverflow,
        )
    }
+35 −8
Original line number Diff line number Diff line
@@ -167,6 +167,9 @@ interface ElementStateScope {
@Stable
@ElementDsl
interface BaseSceneScope : ElementStateScope {
    /** The key of this scene. */
    val sceneKey: SceneKey

    /** The state of the [SceneTransitionLayout] in which this scene is contained. */
    val layoutState: SceneTransitionLayoutState

@@ -285,9 +288,7 @@ interface SceneScope : BaseSceneScope {
     *
     * @param value the value of this shared value in the current scene.
     * @param key the key of this shared value.
     * @param lerp the *linear* interpolation function that should be used to interpolate between
     *   two different values. Note that it has to be linear because the [fraction] passed to this
     *   interpolator is already interpolated.
     * @param type the [SharedValueType] of this animated value.
     * @param canOverflow whether this value can overflow past the values it is interpolated
     *   between, for instance because the transition is animated using a bouncy spring.
     * @see animateSceneIntAsState
@@ -299,11 +300,39 @@ interface SceneScope : BaseSceneScope {
    fun <T> animateSceneValueAsState(
        value: T,
        key: ValueKey,
        lerp: (start: T, stop: T, fraction: Float) -> T,
        type: SharedValueType<T, *>,
        canOverflow: Boolean,
    ): AnimatedState<T>
}

/**
 * The type of a shared value animated using [ElementScope.animateElementValueAsState] or
 * [SceneScope.animateSceneValueAsState].
 */
@Stable
interface SharedValueType<T, Delta> {
    /** The unspecified value for this type. */
    val unspecifiedValue: T

    /**
     * The zero value of this type. It should be equal to what [diff(x, x)] returns for any value of
     * x.
     */
    val zeroDeltaValue: Delta

    /**
     * Return the linear interpolation of [a] and [b] at the given [progress], i.e. `a + (b - a) *
     * progress`.
     */
    fun lerp(a: T, b: T, progress: Float): T

    /** Return `a - b`. */
    fun diff(a: T, b: T): Delta

    /** Return `a + b * bWeight`. */
    fun addWeighted(a: T, b: Delta, bWeight: Float): T
}

@Stable
@ElementDsl
interface ElementScope<ContentScope> {
@@ -312,9 +341,7 @@ interface ElementScope<ContentScope> {
     *
     * @param value the value of this shared value in the current scene.
     * @param key the key of this shared value.
     * @param lerp the *linear* interpolation function that should be used to interpolate between
     *   two different values. Note that it has to be linear because the [fraction] passed to this
     *   interpolator is already interpolated.
     * @param type the [SharedValueType] of this animated value.
     * @param canOverflow whether this value can overflow past the values it is interpolated
     *   between, for instance because the transition is animated using a bouncy spring.
     * @see animateElementIntAsState
@@ -326,7 +353,7 @@ interface ElementScope<ContentScope> {
    fun <T> animateElementValueAsState(
        value: T,
        key: ValueKey,
        lerp: (start: T, stop: T, fraction: Float) -> T,
        type: SharedValueType<T, *>,
        canOverflow: Boolean,
    ): AnimatedState<T>

+5 −6
Original line number Diff line number Diff line
@@ -85,15 +85,14 @@ internal class SceneTransitionLayoutImpl(
     * The different values of a shared value keyed by a a [ValueKey] and the different elements and
     * scenes it is associated to.
     */
    private var _sharedValues:
        MutableMap<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>? =
    private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? =
        null
    internal val sharedValues:
        MutableMap<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>
    internal val sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>
        get() =
            _sharedValues
                ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>()
                    .also { _sharedValues = it }
                ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>().also {
                    _sharedValues = it
                }

    // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed.
    private val horizontalDraggableHandler: DraggableHandlerImpl
Loading