• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compose.animation.scene
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationVector1D
21 import androidx.compose.animation.core.SpringSpec
22 import kotlin.math.absoluteValue
23 import kotlinx.coroutines.CoroutineScope
24 import kotlinx.coroutines.launch
25 
26 /**
27  * Transition to [target] using a canned animation. This function will try to be smart and take over
28  * the currently running transition, if there is one.
29  */
animateToScenenull30 internal fun CoroutineScope.animateToScene(
31     layoutImpl: SceneTransitionLayoutImpl,
32     target: SceneKey,
33 ) {
34     val state = layoutImpl.state.transitionState
35     if (state.currentScene == target) {
36         // This can happen in 3 different situations, for which there isn't anything else to do:
37         //  1. There is no ongoing transition and [target] is already the current scene.
38         //  2. The user is swiping to [target] from another scene and released their pointer such
39         //     that the gesture was committed and the transition is animating to [scene] already.
40         //  3. The user is swiping from [target] to another scene and either:
41         //     a. didn't release their pointer yet.
42         //     b. released their pointer such that the swipe gesture was cancelled and the
43         //        transition is currently animating back to [target].
44         return
45     }
46 
47     when (state) {
48         is TransitionState.Idle -> animate(layoutImpl, target)
49         is TransitionState.Transition -> {
50             if (state.toScene == state.fromScene) {
51                 // Same as idle.
52                 animate(layoutImpl, target)
53                 return
54             }
55 
56             // A transition is currently running: first check whether `transition.toScene` or
57             // `transition.fromScene` is the same as our target scene, in which case the transition
58             // can be accelerated or reversed to end up in the target state.
59 
60             if (state.toScene == target) {
61                 // The user is currently swiping to [target] but didn't release their pointer yet:
62                 // animate the progress to `1`.
63 
64                 check(state.fromScene == state.currentScene)
65                 val progress = state.progress
66                 if ((1f - progress).absoluteValue < ProgressVisibilityThreshold) {
67                     // The transition is already finished (progress ~= 1): no need to animate.
68                     layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
69                 } else {
70                     // The transition is in progress: start the canned animation at the same
71                     // progress as it was in.
72                     // TODO(b/290184746): Also take the current velocity into account.
73                     animate(layoutImpl, target, startProgress = progress)
74                 }
75 
76                 return
77             }
78 
79             if (state.fromScene == target) {
80                 // There is a transition from [target] to another scene: simply animate the same
81                 // transition progress to `0`.
82 
83                 check(state.toScene == state.currentScene)
84                 val progress = state.progress
85                 if (progress.absoluteValue < ProgressVisibilityThreshold) {
86                     // The transition is at progress ~= 0: no need to animate.
87                     layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
88                 } else {
89                     // TODO(b/290184746): Also take the current velocity into account.
90                     animate(layoutImpl, target, startProgress = progress, reversed = true)
91                 }
92 
93                 return
94             }
95 
96             // Generic interruption; the current transition is neither from or to [target].
97             // TODO(b/290930950): Better handle interruptions here.
98             animate(layoutImpl, target)
99         }
100     }
101 }
102 
CoroutineScopenull103 private fun CoroutineScope.animate(
104     layoutImpl: SceneTransitionLayoutImpl,
105     target: SceneKey,
106     startProgress: Float = 0f,
107     reversed: Boolean = false,
108 ) {
109     val fromScene = layoutImpl.state.transitionState.currentScene
110 
111     val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec
112     val visibilityThreshold =
113         (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
114     val animatable = Animatable(startProgress, visibilityThreshold = visibilityThreshold)
115 
116     val targetProgress = if (reversed) 0f else 1f
117     val transition =
118         if (reversed) {
119             OneOffTransition(target, fromScene, currentScene = target, animatable)
120         } else {
121             OneOffTransition(fromScene, target, currentScene = target, animatable)
122         }
123 
124     // Change the current layout state to use this new transition.
125     layoutImpl.state.transitionState = transition
126 
127     // Animate the progress to its target value.
128     launch {
129         animatable.animateTo(targetProgress, animationSpec)
130 
131         // Unless some other external state change happened, the state should now be idle.
132         if (layoutImpl.state.transitionState == transition) {
133             layoutImpl.state.transitionState = TransitionState.Idle(target)
134         }
135     }
136 }
137 
138 private class OneOffTransition(
139     override val fromScene: SceneKey,
140     override val toScene: SceneKey,
141     override val currentScene: SceneKey,
142     private val animatable: Animatable<Float, AnimationVector1D>,
143 ) : TransitionState.Transition {
144     override val progress: Float
145         get() = animatable.value
146 }
147 
148 // TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
149 // and screen density.
150 private const val ProgressVisibilityThreshold = 1e-3f
151