Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt 0 → 100644 +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 packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +24 −68 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -50,7 +45,7 @@ internal fun CoroutineScope.animateToScene( return when (transitionState) { is TransitionState.Idle -> { animate( animateToScene( layoutState, target, transitionKey, Loading Loading @@ -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, ) } Loading @@ -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, ) Loading Loading @@ -140,7 +131,7 @@ internal fun CoroutineScope.animateToScene( animateToScene(layoutState, animateFrom, transitionKey = null) } animate( animateToScene( layoutState, target, transitionKey, Loading @@ -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() } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt 0 → 100644 +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
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +24 −68 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -50,7 +45,7 @@ internal fun CoroutineScope.animateToScene( return when (transitionState) { is TransitionState.Idle -> { animate( animateToScene( layoutState, target, transitionKey, Loading Loading @@ -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, ) } Loading @@ -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, ) Loading Loading @@ -140,7 +131,7 @@ internal fun CoroutineScope.animateToScene( animateToScene(layoutState, animateFrom, transitionKey = null) } animate( animateToScene( layoutState, target, transitionKey, Loading @@ -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() }