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

Commit feb385e7 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Extract AnimateContent out of AnimateToScene

This CL is a pure refactoring that extracts some utility methods to
animate content using one-off transitions. This will be used to animate
overlays that are enabled/disabled.

Bug: 353679003
Test: atest PlatformComposeSceneTransitionLayoutTests
Flag: com.android.systemui.scene_container
Change-Id: Ieb1bcc679e2c26f3d0dd569fa90e6e0d10aa8f1f
parent aa86e4a7
Loading
Loading
Loading
Loading
+91 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.compose.animation.scene

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.SpringSpec
import com.android.compose.animation.scene.content.state.ContentState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

internal fun CoroutineScope.animateContent(
    transition: ContentState.Transition<*>,
    oneOffAnimation: OneOffAnimation,
    targetProgress: Float,
    startTransition: () -> Unit,
    finishTransition: () -> Unit,
) {
    // Start the transition. This will compute the TransformationSpec associated to [transition],
    // which we need to initialize the Animatable that will actually animate it.
    startTransition()

    // The transition now contains the transformation spec that we should use to instantiate the
    // Animatable.
    val animationSpec = transition.transformationSpec.progressSpec
    val visibilityThreshold =
        (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
    val replacedTransition = transition.replacedTransition
    val initialProgress = replacedTransition?.progress ?: 0f
    val initialVelocity = replacedTransition?.progressVelocity ?: 0f
    val animatable =
        Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
            oneOffAnimation.animatable = it
        }

    // Animate the progress to its target value.
    //
    // Important: We start atomically to make sure that we start the coroutine even if it is
    // cancelled right after it is launched, so that finishTransition() is correctly called.
    // Otherwise, this transition will never be stopped and we will never settle to Idle.
    oneOffAnimation.job =
        launch(start = CoroutineStart.ATOMIC) {
            try {
                animatable.animateTo(targetProgress, animationSpec, initialVelocity)
            } finally {
                finishTransition()
            }
        }
}

internal class OneOffAnimation {
    /**
     * The animatable used to animate this transition.
     *
     * Note: This is lateinit because we need to first create this object so that
     * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to
     * the transition, which is needed to initialize this Animatable.
     */
    lateinit var animatable: Animatable<Float, AnimationVector1D>

    /** The job that is animating [animatable]. */
    lateinit var job: Job

    val progress: Float
        get() = animatable.value

    val progressVelocity: Float
        get() = animatable.velocity

    fun finish(): Job = job
}

// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
// and screen density.
internal const val ProgressVisibilityThreshold = 1e-3f
+24 −68
Original line number Diff line number Diff line
@@ -16,15 +16,10 @@

package com.android.compose.animation.scene

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.SpringSpec
import com.android.compose.animation.scene.content.state.TransitionState
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

/**
 * Transition to [target] using a canned animation. This function will try to be smart and take over
@@ -50,7 +45,7 @@ internal fun CoroutineScope.animateToScene(

    return when (transitionState) {
        is TransitionState.Idle -> {
            animate(
            animateToScene(
                layoutState,
                target,
                transitionKey,
@@ -80,13 +75,11 @@ internal fun CoroutineScope.animateToScene(
                } else {
                    // The transition is in progress: start the canned animation at the same
                    // progress as it was in.
                    animate(
                    animateToScene(
                        layoutState,
                        target,
                        transitionKey,
                        isInitiatedByUserInput,
                        initialProgress = progress,
                        initialVelocity = transitionState.progressVelocity,
                        replacedTransition = transitionState,
                    )
                }
@@ -102,13 +95,11 @@ internal fun CoroutineScope.animateToScene(
                    layoutState.finishTransition(transitionState, target)
                    null
                } else {
                    animate(
                    animateToScene(
                        layoutState,
                        target,
                        transitionKey,
                        isInitiatedByUserInput,
                        initialProgress = progress,
                        initialVelocity = transitionState.progressVelocity,
                        reversed = true,
                        replacedTransition = transitionState,
                    )
@@ -140,7 +131,7 @@ internal fun CoroutineScope.animateToScene(
                    animateToScene(layoutState, animateFrom, transitionKey = null)
                }

                animate(
                animateToScene(
                    layoutState,
                    target,
                    transitionKey,
@@ -154,103 +145,68 @@ internal fun CoroutineScope.animateToScene(
    }
}

private fun CoroutineScope.animate(
private fun CoroutineScope.animateToScene(
    layoutState: MutableSceneTransitionLayoutStateImpl,
    targetScene: SceneKey,
    transitionKey: TransitionKey?,
    isInitiatedByUserInput: Boolean,
    replacedTransition: TransitionState.Transition?,
    initialProgress: Float = 0f,
    initialVelocity: Float = 0f,
    reversed: Boolean = false,
    fromScene: SceneKey = layoutState.transitionState.currentScene,
    chain: Boolean = true,
): TransitionState.Transition {
    val oneOffAnimation = OneOffAnimation()
    val targetProgress = if (reversed) 0f else 1f
    val transition =
        if (reversed) {
            OneOffTransition(
            OneOffSceneTransition(
                key = transitionKey,
                fromScene = targetScene,
                toScene = fromScene,
                currentScene = targetScene,
                isInitiatedByUserInput = isInitiatedByUserInput,
                isUserInputOngoing = false,
                replacedTransition = replacedTransition,
                oneOffAnimation = oneOffAnimation,
            )
        } else {
            OneOffTransition(
            OneOffSceneTransition(
                key = transitionKey,
                fromScene = fromScene,
                toScene = targetScene,
                currentScene = targetScene,
                isInitiatedByUserInput = isInitiatedByUserInput,
                isUserInputOngoing = false,
                replacedTransition = replacedTransition,
                oneOffAnimation = oneOffAnimation,
            )
        }

    // Change the current layout state to start this new transition. This will compute the
    // TransformationSpec associated to this transition, which we need to initialize the Animatable
    // that will actually animate it.
    layoutState.startTransition(transition, chain)

    // The transition now contains the transformation spec that we should use to instantiate the
    // Animatable.
    val animationSpec = transition.transformationSpec.progressSpec
    val visibilityThreshold =
        (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
    val animatable =
        Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
            transition.animatable = it
        }

    // Animate the progress to its target value.
    // Important: We start atomically to make sure that we start the coroutine even if it is
    // cancelled right after it is launched, so that finishTransition() is correctly called.
    // Otherwise, this transition will never be stopped and we will never settle to Idle.
    transition.job =
        launch(start = CoroutineStart.ATOMIC) {
            try {
                animatable.animateTo(targetProgress, animationSpec, initialVelocity)
            } finally {
                layoutState.finishTransition(transition, targetScene)
            }
        }
    animateContent(
        transition = transition,
        oneOffAnimation = oneOffAnimation,
        targetProgress = targetProgress,
        startTransition = { layoutState.startTransition(transition, chain) },
        finishTransition = { layoutState.finishTransition(transition, targetScene) },
    )

    return transition
}

private class OneOffTransition(
private class OneOffSceneTransition(
    override val key: TransitionKey?,
    fromScene: SceneKey,
    toScene: SceneKey,
    override val currentScene: SceneKey,
    override val isInitiatedByUserInput: Boolean,
    override val isUserInputOngoing: Boolean,
    replacedTransition: TransitionState.Transition?,
    private val oneOffAnimation: OneOffAnimation,
) : TransitionState.Transition(fromScene, toScene, replacedTransition) {
    /**
     * The animatable used to animate this transition.
     *
     * Note: This is lateinit because we need to first create this Transition object so that
     * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to
     * it, which is need to initialize this Animatable.
     */
    lateinit var animatable: Animatable<Float, AnimationVector1D>

    /** The job that is animating [animatable]. */
    lateinit var job: Job

    override val progress: Float
        get() = animatable.value
        get() = oneOffAnimation.progress

    override val progressVelocity: Float
        get() = animatable.velocity
        get() = oneOffAnimation.progressVelocity

    override fun finish(): Job = job
}
    override val isUserInputOngoing: Boolean = false

// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
// and screen density.
internal const val ProgressVisibilityThreshold = 1e-3f
    override fun finish(): Job = oneOffAnimation.finish()
}