• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.content.state
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationVector1D
21 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
22 import androidx.compose.runtime.Stable
23 import androidx.compose.runtime.derivedStateOf
24 import androidx.compose.runtime.getValue
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.setValue
27 import com.android.compose.animation.scene.ContentKey
28 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
29 import com.android.compose.animation.scene.OverlayKey
30 import com.android.compose.animation.scene.ProgressVisibilityThreshold
31 import com.android.compose.animation.scene.SceneKey
32 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
33 import com.android.compose.animation.scene.TransformationSpec
34 import com.android.compose.animation.scene.TransformationSpecImpl
35 import com.android.compose.animation.scene.TransitionKey
36 import com.android.internal.jank.Cuj.CujType
37 import com.android.mechanics.GestureContext
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.coroutineScope
40 import kotlinx.coroutines.launch
41 
42 /** The state associated to a [SceneTransitionLayout] at some specific point in time. */
43 @Stable
44 sealed interface TransitionState {
45     /**
46      * The current effective scene. If a new scene transition was triggered, it would start from
47      * this scene.
48      *
49      * For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe
50      * gesture starts, but then if the user flings their finger and commits the transition to scene
51      * B, then [currentScene] becomes scene B even if the transition is not finished yet and is
52      * still animating to settle to scene B.
53      */
54     val currentScene: SceneKey
55 
56     /**
57      * The current set of overlays. This represents the set of overlays that will be visible on
58      * screen once all transitions are finished.
59      *
60      * @see MutableSceneTransitionLayoutState.showOverlay
61      * @see MutableSceneTransitionLayoutState.hideOverlay
62      * @see MutableSceneTransitionLayoutState.replaceOverlay
63      */
64     val currentOverlays: Set<OverlayKey>
65 
66     /** The scene [currentScene] is idle. */
67     data class Idle(
68         override val currentScene: SceneKey,
69         override val currentOverlays: Set<OverlayKey> = emptySet(),
70     ) : TransitionState
71 
72     sealed class Transition(
73         val fromContent: ContentKey,
74         val toContent: ContentKey,
75         val replacedTransition: Transition? = null,
76     ) : TransitionState {
77         /** A transition animating between [fromScene] and [toScene]. */
78         abstract class ChangeScene(
79             /** The scene this transition is starting from. Can't be the same as toScene */
80             val fromScene: SceneKey,
81 
82             /** The scene this transition is going to. Can't be the same as fromScene */
83             val toScene: SceneKey,
84 
85             /** The transition that `this` transition is replacing, if any. */
86             replacedTransition: Transition? = null,
87         ) : Transition(fromScene, toScene, replacedTransition) {
88             final override val currentOverlays: Set<OverlayKey>
89                 get() {
90                     // The set of overlays does not change in a [ChangeCurrentScene] transition.
91                     return currentOverlaysWhenTransitionStarted
92                 }
93 
toStringnull94             override fun toString(): String {
95                 return "ChangeScene(fromScene=$fromScene, toScene=$toScene)"
96             }
97         }
98 
99         /**
100          * A transition that is animating one or more overlays and for which [currentOverlays] will
101          * change over the course of the transition.
102          */
103         sealed class OverlayTransition(
104             fromContent: ContentKey,
105             toContent: ContentKey,
106             replacedTransition: Transition?,
107         ) : Transition(fromContent, toContent, replacedTransition) {
108             final override val currentScene: SceneKey
109                 get() {
110                     // The current scene does not change during overlay transitions.
111                     return currentSceneWhenTransitionStarted
112                 }
113 
114             // Note: We use deriveStateOf() so that the computed set is cached and reused when the
115             // inputs of the computations don't change, to avoid recomputing and allocating a new
116             // set every time currentOverlays is called (which is every frame and for each element).
<lambda>null117             final override val currentOverlays: Set<OverlayKey> by derivedStateOf {
118                 computeCurrentOverlays()
119             }
120 
computeCurrentOverlaysnull121             protected abstract fun computeCurrentOverlays(): Set<OverlayKey>
122         }
123 
124         /** The [overlay] is either showing from [fromOrToScene] or hiding into [fromOrToScene]. */
125         abstract class ShowOrHideOverlay(
126             val overlay: OverlayKey,
127             val fromOrToScene: SceneKey,
128             fromContent: ContentKey,
129             toContent: ContentKey,
130             replacedTransition: Transition? = null,
131         ) : OverlayTransition(fromContent, toContent, replacedTransition) {
132             /**
133              * Whether [overlay] is effectively shown. For instance, this will be `false` when
134              * starting a swipe transition to show [overlay] and will be `true` only once the swipe
135              * transition is committed.
136              */
137             abstract val isEffectivelyShown: Boolean
138 
139             init {
140                 check(
141                     (fromContent == fromOrToScene && toContent == overlay) ||
142                         (fromContent == overlay && toContent == fromOrToScene)
143                 )
144             }
145 
146             final override fun computeCurrentOverlays(): Set<OverlayKey> {
147                 return if (isEffectivelyShown) {
148                     currentOverlaysWhenTransitionStarted + overlay
149                 } else {
150                     currentOverlaysWhenTransitionStarted - overlay
151                 }
152             }
153 
154             override fun toString(): String {
155                 val isShowing = overlay == toContent
156                 return "ShowOrHideOverlay(overlay=$overlay, fromOrToScene=$fromOrToScene, " +
157                     "isShowing=$isShowing)"
158             }
159         }
160 
161         /** We are transitioning from [fromOverlay] to [toOverlay]. */
162         abstract class ReplaceOverlay(
163             val fromOverlay: OverlayKey,
164             val toOverlay: OverlayKey,
165             replacedTransition: Transition? = null,
166         ) :
167             OverlayTransition(
168                 fromContent = fromOverlay,
169                 toContent = toOverlay,
170                 replacedTransition,
171             ) {
172             /**
173              * The current effective overlay, either [fromOverlay] or [toOverlay]. For instance,
174              * this will be [fromOverlay] when starting a swipe transition that replaces
175              * [fromOverlay] by [toOverlay] and will [toOverlay] once the swipe transition is
176              * committed.
177              */
178             abstract val effectivelyShownOverlay: OverlayKey
179 
180             init {
181                 check(fromOverlay != toOverlay)
182             }
183 
computeCurrentOverlaysnull184             final override fun computeCurrentOverlays(): Set<OverlayKey> {
185                 return when (effectivelyShownOverlay) {
186                     fromOverlay ->
187                         computeCurrentOverlays(include = fromOverlay, exclude = toOverlay)
188                     toOverlay -> computeCurrentOverlays(include = toOverlay, exclude = fromOverlay)
189                     else ->
190                         error(
191                             "effectivelyShownOverlay=$effectivelyShownOverlay, should be " +
192                                 "equal to fromOverlay=$fromOverlay or toOverlay=$toOverlay"
193                         )
194                 }
195             }
196 
computeCurrentOverlaysnull197             private fun computeCurrentOverlays(
198                 include: OverlayKey,
199                 exclude: OverlayKey,
200             ): Set<OverlayKey> {
201                 return buildSet {
202                     addAll(currentOverlaysWhenTransitionStarted)
203                     remove(exclude)
204                     add(include)
205                 }
206             }
207 
toStringnull208             override fun toString(): String {
209                 return "ReplaceOverlay(fromOverlay=$fromOverlay, toOverlay=$toOverlay)"
210             }
211         }
212 
213         /**
214          * The current scene and overlays observed right when this transition started. These are set
215          * when this transition is started in
216          * [com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl.startTransition].
217          */
218         internal lateinit var currentSceneWhenTransitionStarted: SceneKey
219         internal lateinit var currentOverlaysWhenTransitionStarted: Set<OverlayKey>
220 
221         /**
222          * The key of this transition. This should usually be null, but it can be specified to use a
223          * specific set of transformations associated to this transition.
224          */
225         open val key: TransitionKey? = null
226 
227         /**
228          * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be
229          * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or
230          * when flinging quickly during a swipe gesture.
231          */
232         abstract val progress: Float
233 
234         /** The current velocity of [progress], in progress units. */
235         abstract val progressVelocity: Float
236 
237         /** Whether the transition was triggered by user input rather than being programmatic. */
238         abstract val isInitiatedByUserInput: Boolean
239 
240         /** Whether user input is currently driving the transition. */
241         abstract val isUserInputOngoing: Boolean
242 
243         /** Additional gesture context whenever the transition is driven by a user gesture. */
244         abstract val gestureContext: GestureContext?
245 
246         /**
247          * True when the transition reached the end and the progress won't be updated anymore.
248          *
249          * [isProgressStable] will be `true` before this [Transition] is completed while there are
250          * still custom transition animations settling.
251          */
252         var isProgressStable: Boolean by mutableStateOf(false)
253             private set
254 
255         /** The CUJ covered by this transition. */
256         @CujType
257         val cuj: Int?
258             get() = _cuj
259 
260         /**
261          * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
262          * also be less than `0` or greater than `1` when using transitions with a spring
263          * AnimationSpec or when flinging quickly during a swipe gesture.
264          */
265         internal open val previewProgress: Float = 0f
266 
267         /** The current velocity of [previewProgress], in progress units. */
268         internal open val previewProgressVelocity: Float = 0f
269 
270         /** Whether the transition is currently in the preview stage */
271         internal open val isInPreviewStage: Boolean = false
272 
273         /**
274          * The current [TransformationSpecImpl] and other values associated to this transition from
275          * the spec.
276          *
277          * Important: These will be set exactly once, when this transition is
278          * [started][MutableSceneTransitionLayoutStateImpl.startTransition].
279          */
280         internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
281         internal var previewTransformationSpec: TransformationSpecImpl? = null
282         internal var _cuj: Int? = null
283 
284         /**
285          * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
286          * jump of values when this transitions interrupts another one.
287          */
288         private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
289 
290         /**
291          * The coroutine scope associated to this transition.
292          *
293          * This coroutine scope can be used to launch animations associated to this transition,
294          * which will not finish until at least one animation/job is still running in the scope.
295          *
296          * Important: Make sure to never launch long-running jobs in this scope, otherwise the
297          * transition will never be considered as finished.
298          */
299         internal val coroutineScope: CoroutineScope
300             get() =
301                 _coroutineScope
302                     ?: error(
303                         "Transition.coroutineScope can only be accessed once the transition was " +
304                             "started "
305                     )
306 
307         private var _coroutineScope: CoroutineScope? = null
308 
309         init {
310             check(fromContent != toContent)
311             check(
312                 replacedTransition == null ||
313                     (replacedTransition.fromContent == fromContent &&
314                         replacedTransition.toContent == toContent)
315             )
316         }
317 
318         /**
319          * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
320          * match the contents we are animating from and/or to.
321          */
isTransitioningnull322         fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
323             return (from == null || fromContent == from) && (to == null || toContent == to)
324         }
325 
326         /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
isTransitioningBetweennull327         fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
328             return isTransitioning(from = content, to = other) ||
329                 isTransitioning(from = other, to = content)
330         }
331 
332         /** Whether we are transitioning from or to [content]. */
isTransitioningFromOrTonull333         fun isTransitioningFromOrTo(content: ContentKey): Boolean {
334             return fromContent == content || toContent == content
335         }
336 
337         /**
338          * Return [progress] if [content] is equal to [toContent], `1f - progress` if [content] is
339          * equal to [fromContent], and throw otherwise.
340          */
progressTonull341         fun progressTo(content: ContentKey): Float {
342             return when (content) {
343                 toContent -> progress
344                 fromContent -> 1f - progress
345                 else ->
346                     throw IllegalArgumentException(
347                         "content ($content) should be either toContent ($toContent) or " +
348                             "fromContent ($fromContent)"
349                     )
350             }
351         }
352 
353         /** Whether [fromContent] is effectively the current content of the transition. */
isFromCurrentContentnull354         internal fun isFromCurrentContent() = isCurrentContent(expectedFrom = true)
355 
356         /** Whether [toContent] is effectively the current content of the transition. */
357         internal fun isToCurrentContent() = isCurrentContent(expectedFrom = false)
358 
359         private fun isCurrentContent(expectedFrom: Boolean): Boolean {
360             val expectedContent = if (expectedFrom) fromContent else toContent
361             return when (this) {
362                 is ChangeScene -> currentScene == expectedContent
363                 is ReplaceOverlay -> effectivelyShownOverlay == expectedContent
364                 is ShowOrHideOverlay -> isEffectivelyShown == (expectedContent == overlay)
365             }
366         }
367 
368         /** Run this transition and return once it is finished. */
runnull369         protected abstract suspend fun run()
370 
371         /**
372          * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will
373          * change in the future, and animate the progress towards that state. For instance, a
374          * [Transition.ChangeScene] should animate the progress to 0f if its [currentScene] is equal
375          * to its [fromScene][Transition.ChangeScene.fromScene] or animate it to 1f if its equal to
376          * its [toScene][Transition.ChangeScene.toScene].
377          *
378          * This is called when this transition is interrupted (replaced) by another transition.
379          */
380         abstract fun freezeAndAnimateToCurrentState()
381 
382         internal suspend fun runInternal() {
383             check(_coroutineScope == null) { "A Transition can be started only once." }
384             coroutineScope {
385                 _coroutineScope = this
386                 try {
387                     run()
388                 } finally {
389                     isProgressStable = true
390                 }
391             }
392         }
393 
interruptionProgressnull394         internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float {
395             if (replacedTransition != null) {
396                 return replacedTransition.interruptionProgress(layoutImpl)
397             }
398 
399             fun create(): Animatable<Float, AnimationVector1D> {
400                 val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
401                 layoutImpl.animationScope.launch {
402                     @OptIn(ExperimentalMaterial3ExpressiveApi::class)
403                     animatable.animateTo(
404                         targetValue = 0f,
405                         // Quickly animate (use fast) the current transition and without bounces
406                         // (use effects). A new transition will start soon.
407                         animationSpec = layoutImpl.state.motionScheme.fastEffectsSpec(),
408                     )
409                 }
410 
411                 return animatable
412             }
413 
414             val animatable = interruptionDecay ?: create().also { interruptionDecay = it }
415             return animatable.value
416         }
417     }
418 
419     companion object {
420         const val DistanceUnspecified = 0f
421     }
422 }
423