1 /*
<lambda>null2  * 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.transition
18 
19 import androidx.annotation.FloatRange
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.foundation.gestures.Orientation
22 import androidx.compose.ui.util.fastCoerceIn
23 import com.android.compose.animation.scene.ContentKey
24 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
25 import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
26 import com.android.compose.animation.scene.OverlayKey
27 import com.android.compose.animation.scene.SceneKey
28 import com.android.compose.animation.scene.SwipeAnimation
29 import com.android.compose.animation.scene.TransitionKey
30 import com.android.compose.animation.scene.UserActionResult
31 import com.android.compose.animation.scene.createSwipeAnimation
32 import kotlin.coroutines.cancellation.CancellationException
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.Job
35 import kotlinx.coroutines.coroutineScope
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.collectLatest
38 import kotlinx.coroutines.launch
39 
40 /**
41  * Seek to the given [scene] using [progress].
42  *
43  * This will start a transition from the
44  * [current scene][MutableSceneTransitionLayoutState.currentScene] to [scene], driven by the
45  * progress in [progress]. Once [progress] stops emitting, we will animate progress to 1f (using
46  * [animationSpec]) if it stopped normally or to 0f if it stopped with a
47  * [kotlin.coroutines.cancellation.CancellationException].
48  */
49 suspend fun MutableSceneTransitionLayoutState.seekToScene(
50     scene: SceneKey,
51     @FloatRange(0.0, 1.0) progress: Flow<Float>,
52     transitionKey: TransitionKey? = null,
53     animationSpec: AnimationSpec<Float>? = null,
54 ) {
55     require(scene != currentScene) {
56         "seekToScene($scene) has to be called with a different scene than the current scene"
57     }
58 
59     seek(UserActionResult.ChangeScene(scene, transitionKey), progress, animationSpec)
60 }
61 
62 /**
63  * Seek to show the given [overlay] using [progress].
64  *
65  * This will start a transition to show [overlay] from the
66  * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in
67  * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using
68  * [animationSpec]) if it stopped normally or to 0f if it stopped with a
69  * [kotlin.coroutines.cancellation.CancellationException].
70  */
seekToShowOverlaynull71 suspend fun MutableSceneTransitionLayoutState.seekToShowOverlay(
72     overlay: OverlayKey,
73     @FloatRange(0.0, 1.0) progress: Flow<Float>,
74     transitionKey: TransitionKey? = null,
75     animationSpec: AnimationSpec<Float>? = null,
76 ) {
77     require(overlay in currentOverlays) {
78         "seekToShowOverlay($overlay) can be called only when the overlay is in currentOverlays"
79     }
80 
81     seek(UserActionResult.ShowOverlay(overlay, transitionKey), progress, animationSpec)
82 }
83 
84 /**
85  * Seek to hide the given [overlay] using [progress].
86  *
87  * This will start a transition to hide [overlay] to the
88  * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in
89  * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using
90  * [animationSpec]) if it stopped normally or to 0f if it stopped with a
91  * [kotlin.coroutines.cancellation.CancellationException].
92  */
seekToHideOverlaynull93 suspend fun MutableSceneTransitionLayoutState.seekToHideOverlay(
94     overlay: OverlayKey,
95     @FloatRange(0.0, 1.0) progress: Flow<Float>,
96     transitionKey: TransitionKey? = null,
97     animationSpec: AnimationSpec<Float>? = null,
98 ) {
99     require(overlay !in currentOverlays) {
100         "seekToHideOverlay($overlay) can be called only when the overlay is *not* in " +
101             "currentOverlays"
102     }
103 
104     seek(UserActionResult.HideOverlay(overlay, transitionKey), progress, animationSpec)
105 }
106 
seeknull107 private suspend fun MutableSceneTransitionLayoutState.seek(
108     result: UserActionResult,
109     progress: Flow<Float>,
110     animationSpec: AnimationSpec<Float>?,
111 ) {
112     val layoutState =
113         when (this) {
114             is MutableSceneTransitionLayoutStateImpl -> this
115         }
116 
117     val swipeAnimation =
118         createSwipeAnimation(
119             layoutState = layoutState,
120             result = result,
121 
122             // We are animating progress, so distance is always 1f.
123             distance = 1f,
124 
125             // The orientation and isUpOrLeft don't matter here given that they are only used during
126             // overscroll, which is disabled for progress-based transitions.
127             orientation = Orientation.Horizontal,
128             isUpOrLeft = false,
129         )
130 
131     animateProgress(
132         state = layoutState,
133         animation = swipeAnimation,
134         progress = progress,
135         commitSpec = animationSpec,
136         cancelSpec = animationSpec,
137     )
138 }
139 
animateProgressnull140 internal suspend fun <T : ContentKey> animateProgress(
141     state: MutableSceneTransitionLayoutStateImpl,
142     animation: SwipeAnimation<T>,
143     progress: Flow<Float>,
144     commitSpec: AnimationSpec<Float>?,
145     cancelSpec: AnimationSpec<Float>?,
146     animationScope: CoroutineScope? = null,
147 ) {
148     suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) {
149         if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) {
150             return
151         }
152 
153         animation.animateOffset(
154             initialVelocity = 0f,
155             targetContent = targetContent,
156 
157             // Important: we have to specify a spec that correctly animates *progress* (low
158             // visibility threshold) and not *offset* (higher visibility threshold).
159             spec = spec ?: animation.contentTransition.transformationSpec.progressSpec,
160         )
161     }
162 
163     coroutineScope {
164         val collectionJob = launch {
165             try {
166                 progress.collectLatest { progress ->
167                     // Progress based animation should never overscroll given that the
168                     // absoluteDistance exposed to overscroll builders is always 1f and will not
169                     // lead to any noticeable transformation.
170                     animation.dragOffset = progress.fastCoerceIn(0f, 1f)
171                 }
172 
173                 // Transition committed.
174                 animateOffset(animation.toContent, commitSpec)
175             } catch (e: CancellationException) {
176                 // Transition cancelled.
177                 animateOffset(animation.fromContent, cancelSpec)
178             }
179         }
180 
181         // Start the transition.
182         animationScope?.launch { startTransition(state, animation, collectionJob) }
183             ?: startTransition(state, animation, collectionJob)
184     }
185 }
186 
startTransitionnull187 private suspend fun <T : ContentKey> startTransition(
188     state: MutableSceneTransitionLayoutStateImpl,
189     animation: SwipeAnimation<T>,
190     progressCollectionJob: Job,
191 ) {
192     state.startTransition(animation.contentTransition)
193     // The transition is done. Cancel the collection in case the transition was finished
194     // because it was interrupted by another transition.
195     if (progressCollectionJob.isActive) {
196         progressCollectionJob.cancel()
197     }
198 }
199