• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18 
19 import androidx.activity.BackEventCompat
20 import androidx.activity.compose.PredictiveBackHandler
21 import androidx.compose.animation.core.AnimationSpec
22 import androidx.compose.animation.core.snap
23 import androidx.compose.foundation.gestures.Orientation
24 import androidx.compose.runtime.Composable
25 import androidx.compose.ui.util.fastCoerceIn
26 import com.android.compose.animation.scene.UserActionResult.ChangeScene
27 import com.android.compose.animation.scene.UserActionResult.HideOverlay
28 import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
29 import com.android.compose.animation.scene.UserActionResult.ShowOverlay
30 import com.android.mechanics.ProvidedGestureContext
31 import com.android.mechanics.spec.InputDirection
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.flow.first
39 import kotlinx.coroutines.flow.map
40 import kotlinx.coroutines.launch
41 
42 @Composable
43 internal fun PredictiveBackHandler(
44     layoutImpl: SceneTransitionLayoutImpl,
45     result: UserActionResult?,
46 ) {
47     PredictiveBackHandler(enabled = result != null) { events: Flow<BackEventCompat> ->
48         if (result == null) {
49             // Note: We have to collect progress otherwise PredictiveBackHandler will throw.
50             events.first()
51             return@PredictiveBackHandler
52         }
53 
54         if (result is ShowOverlay) {
55             layoutImpl.hideOverlays(result.hideCurrentOverlays)
56         }
57 
58         val animation =
59             createSwipeAnimation(
60                 layoutImpl,
61                 if (result.transitionKey != null) {
62                     result
63                 } else {
64                     result.copy(transitionKey = TransitionKey.PredictiveBack)
65                 },
66                 isUpOrLeft = false,
67                 // Note that the orientation does not matter here given that it's only used to
68                 // compute the distance. In our case the distance is always 1f.
69                 orientation = Orientation.Horizontal,
70                 distance = 1f,
71                 gestureContext =
72                     ProvidedGestureContext(dragOffset = 0f, direction = InputDirection.Max),
73                 decayAnimationSpec = layoutImpl.decayAnimationSpec,
74             )
75 
76         animateProgress(
77             state = layoutImpl.state,
78             animation = animation,
79             progress = events.map { it.progress },
80 
81             // Use the transformationSpec.progressSpec. We will lazily access it later once the
82             // transition has been started, because at this point the transformation spec of the
83             // transition is not computed yet.
84             commitSpec = null,
85 
86             // The predictive back APIs will automatically animate the progress for us in this case
87             // so there is no need to animate it.
88             cancelSpec = snap(),
89             animationScope = layoutImpl.animationScope,
90         )
91     }
92 }
93 
copynull94 private fun UserActionResult.copy(
95     transitionKey: TransitionKey? = this.transitionKey
96 ): UserActionResult {
97     return when (this) {
98         is ChangeScene -> copy(transitionKey = transitionKey)
99         is ShowOverlay -> copy(transitionKey = transitionKey)
100         is HideOverlay -> copy(transitionKey = transitionKey)
101         is ReplaceByOverlay -> copy(transitionKey = transitionKey)
102     }
103 }
104 
animateProgressnull105 private suspend fun <T : ContentKey> animateProgress(
106     state: MutableSceneTransitionLayoutStateImpl,
107     animation: SwipeAnimation<T>,
108     progress: Flow<Float>,
109     commitSpec: AnimationSpec<Float>?,
110     cancelSpec: AnimationSpec<Float>?,
111     animationScope: CoroutineScope? = null,
112 ) {
113     suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) {
114         if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) {
115             return
116         }
117 
118         animation.animateOffset(
119             initialVelocity = 0f,
120             targetContent = targetContent,
121 
122             // Important: we have to specify a spec that correctly animates *progress* (low
123             // visibility threshold) and not *offset* (higher visibility threshold).
124             spec = spec ?: animation.contentTransition.transformationSpec.progressSpec,
125         )
126     }
127 
128     coroutineScope {
129         val collectionJob = launch {
130             try {
131                 progress.collectLatest { progress ->
132                     // Progress based animation should never overscroll given that the
133                     // absoluteDistance exposed to overscroll builders is always 1f and will not
134                     // lead to any noticeable transformation.
135                     animation.dragOffset = progress.fastCoerceIn(0f, 1f)
136                 }
137 
138                 // Transition committed.
139                 animateOffset(animation.toContent, commitSpec)
140             } catch (e: CancellationException) {
141                 // Transition cancelled.
142                 animateOffset(animation.fromContent, cancelSpec)
143             }
144         }
145 
146         // Start the transition.
147         animationScope?.launch { startTransition(state, animation, collectionJob) }
148             ?: startTransition(state, animation, collectionJob)
149     }
150 }
151 
startTransitionnull152 private suspend fun <T : ContentKey> startTransition(
153     state: MutableSceneTransitionLayoutStateImpl,
154     animation: SwipeAnimation<T>,
155     progressCollectionJob: Job,
156 ) {
157     state.startTransition(animation.contentTransition)
158     // The transition is done. Cancel the collection in case the transition was finished
159     // because it was interrupted by another transition.
160     if (progressCollectionJob.isActive) {
161         progressCollectionJob.cancel()
162     }
163 }
164