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.animation.core.Spring
22 import androidx.compose.animation.core.spring
23 import androidx.compose.foundation.gestures.Orientation
24 import androidx.compose.runtime.Stable
25 import androidx.compose.runtime.State
26 import androidx.compose.runtime.derivedStateOf
27 import androidx.compose.runtime.getValue
28 import com.android.compose.animation.scene.ContentKey
29 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
30 import com.android.compose.animation.scene.OverlayKey
31 import com.android.compose.animation.scene.OverscrollSpecImpl
32 import com.android.compose.animation.scene.ProgressVisibilityThreshold
33 import com.android.compose.animation.scene.SceneKey
34 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
35 import com.android.compose.animation.scene.TransformationSpec
36 import com.android.compose.animation.scene.TransformationSpecImpl
37 import com.android.compose.animation.scene.TransitionKey
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         /**
244          * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
245          * also be less than `0` or greater than `1` when using transitions with a spring
246          * AnimationSpec or when flinging quickly during a swipe gesture.
247          */
248         internal open val previewProgress: Float = 0f
249 
250         /** The current velocity of [previewProgress], in progress units. */
251         internal open val previewProgressVelocity: Float = 0f
252 
253         /** Whether the transition is currently in the preview stage */
254         internal open val isInPreviewStage: Boolean = false
255 
256         /**
257          * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this
258          * transition.
259          *
260          * Important: These will be set exactly once, when this transition is
261          * [started][MutableSceneTransitionLayoutStateImpl.startTransition].
262          */
263         internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
264         internal var previewTransformationSpec: TransformationSpecImpl? = null
265         private var fromOverscrollSpec: OverscrollSpecImpl? = null
266         private var toOverscrollSpec: OverscrollSpecImpl? = null
267 
268         /**
269          * The current [OverscrollSpecImpl], if this transition is currently overscrolling.
270          *
271          * Note: This is backed by a State<OverscrollSpecImpl?> because the overscroll spec is
272          * derived from progress, and we don't want readers of currentOverscrollSpec to recompose
273          * every time progress is changed.
274          */
275         private val _currentOverscrollSpec: State<OverscrollSpecImpl?>? =
276             if (this !is HasOverscrollProperties) {
277                 null
278             } else {
<lambda>null279                 derivedStateOf {
280                     val progress = progress
281                     val bouncingContent = bouncingContent
282                     when {
283                         progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec
284                         progress > 1f || bouncingContent == toContent -> toOverscrollSpec
285                         else -> null
286                     }
287                 }
288             }
289         internal val currentOverscrollSpec: OverscrollSpecImpl?
290             get() = _currentOverscrollSpec?.value
291 
292         /**
293          * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
294          * jump of values when this transitions interrupts another one.
295          */
296         private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
297 
298         /**
299          * The coroutine scope associated to this transition.
300          *
301          * This coroutine scope can be used to launch animations associated to this transition,
302          * which will not finish until at least one animation/job is still running in the scope.
303          *
304          * Important: Make sure to never launch long-running jobs in this scope, otherwise the
305          * transition will never be considered as finished.
306          */
307         internal val coroutineScope: CoroutineScope
308             get() =
309                 _coroutineScope
310                     ?: error(
311                         "Transition.coroutineScope can only be accessed once the transition was " +
312                             "started "
313                     )
314 
315         private var _coroutineScope: CoroutineScope? = null
316 
317         init {
318             check(fromContent != toContent)
319             check(
320                 replacedTransition == null ||
321                     (replacedTransition.fromContent == fromContent &&
322                         replacedTransition.toContent == toContent)
323             )
324         }
325 
326         /**
327          * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
328          * match the contents we are animating from and/or to.
329          */
isTransitioningnull330         fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
331             return (from == null || fromContent == from) && (to == null || toContent == to)
332         }
333 
334         /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
isTransitioningBetweennull335         fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
336             return isTransitioning(from = content, to = other) ||
337                 isTransitioning(from = other, to = content)
338         }
339 
340         /** Whether we are transitioning from or to [content]. */
isTransitioningFromOrTonull341         fun isTransitioningFromOrTo(content: ContentKey): Boolean {
342             return fromContent == content || toContent == content
343         }
344 
345         /**
346          * Return [progress] if [content] is equal to [toContent], `1f - progress` if [content] is
347          * equal to [fromContent], and throw otherwise.
348          */
progressTonull349         fun progressTo(content: ContentKey): Float {
350             return when (content) {
351                 toContent -> progress
352                 fromContent -> 1f - progress
353                 else ->
354                     throw IllegalArgumentException(
355                         "content ($content) should be either toContent ($toContent) or " +
356                             "fromContent ($fromContent)"
357                     )
358             }
359         }
360 
361         /** Whether [fromContent] is effectively the current content of the transition. */
isFromCurrentContentnull362         internal fun isFromCurrentContent() = isCurrentContent(expectedFrom = true)
363 
364         /** Whether [toContent] is effectively the current content of the transition. */
365         internal fun isToCurrentContent() = isCurrentContent(expectedFrom = false)
366 
367         private fun isCurrentContent(expectedFrom: Boolean): Boolean {
368             val expectedContent = if (expectedFrom) fromContent else toContent
369             return when (this) {
370                 is ChangeScene -> currentScene == expectedContent
371                 is ReplaceOverlay -> effectivelyShownOverlay == expectedContent
372                 is ShowOrHideOverlay -> isEffectivelyShown == (expectedContent == overlay)
373             }
374         }
375 
376         /** Run this transition and return once it is finished. */
runnull377         protected abstract suspend fun run()
378 
379         /**
380          * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will
381          * change in the future, and animate the progress towards that state. For instance, a
382          * [Transition.ChangeScene] should animate the progress to 0f if its [currentScene] is equal
383          * to its [fromScene][Transition.ChangeScene.fromScene] or animate it to 1f if its equal to
384          * its [toScene][Transition.ChangeScene.toScene].
385          *
386          * This is called when this transition is interrupted (replaced) by another transition.
387          */
388         abstract fun freezeAndAnimateToCurrentState()
389 
390         internal suspend fun runInternal() {
391             check(_coroutineScope == null) { "A Transition can be started only once." }
392             coroutineScope {
393                 _coroutineScope = this
394                 run()
395             }
396         }
397 
updateOverscrollSpecsnull398         internal fun updateOverscrollSpecs(
399             fromSpec: OverscrollSpecImpl?,
400             toSpec: OverscrollSpecImpl?,
401         ) {
402             fromOverscrollSpec = fromSpec
403             toOverscrollSpec = toSpec
404         }
405 
406         /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */
isWithinProgressRangenull407         internal fun isWithinProgressRange(progress: Float): Boolean {
408             // If the properties are missing we assume that every [Transition] can overscroll
409             if (this !is HasOverscrollProperties) return true
410             // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet.
411             val specForCurrentScene =
412                 when {
413                     progress <= 0f -> fromOverscrollSpec
414                     progress >= 1f -> toOverscrollSpec
415                     else -> null
416                 } ?: return true
417 
418             return specForCurrentScene.transformationSpec.transformationMatchers.isNotEmpty()
419         }
420 
interruptionProgressnull421         internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float {
422             if (replacedTransition != null) {
423                 return replacedTransition.interruptionProgress(layoutImpl)
424             }
425 
426             fun create(): Animatable<Float, AnimationVector1D> {
427                 val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
428                 layoutImpl.animationScope.launch {
429                     val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec
430                     val progressSpec =
431                         spring(
432                             stiffness = swipeSpec.stiffness,
433                             dampingRatio = Spring.DampingRatioNoBouncy,
434                             visibilityThreshold = ProgressVisibilityThreshold,
435                         )
436                     animatable.animateTo(0f, progressSpec)
437                 }
438 
439                 return animatable
440             }
441 
442             val animatable = interruptionDecay ?: create().also { interruptionDecay = it }
443             return animatable.value
444         }
445     }
446 
447     interface HasOverscrollProperties {
448         /**
449          * The position of the [Transition.toContent].
450          *
451          * Used to understand the direction of the overscroll.
452          */
453         val isUpOrLeft: Boolean
454 
455         /**
456          * The relative orientation between [Transition.fromContent] and [Transition.toContent].
457          *
458          * Used to understand the orientation of the overscroll.
459          */
460         val orientation: Orientation
461 
462         /**
463          * Return the absolute distance between fromScene and toScene, if available, otherwise
464          * [DistanceUnspecified].
465          */
466         val absoluteDistance: Float
467 
468         /**
469          * The content (scene or overlay) around which the transition is currently bouncing. When
470          * not `null`, this transition is currently oscillating around this content and will soon
471          * settle to that content.
472          */
473         val bouncingContent: ContentKey?
474 
475         companion object {
476             const val DistanceUnspecified = 0f
477         }
478     }
479 }
480