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