1 /*
<lambda>null2  * Copyright 2023 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.compose.animation.core.AnimationSpec
20 import androidx.compose.animation.core.Spring
21 import androidx.compose.animation.core.SpringSpec
22 import androidx.compose.animation.core.snap
23 import androidx.compose.animation.core.spring
24 import androidx.compose.foundation.gestures.Orientation
25 import androidx.compose.ui.geometry.Offset
26 import androidx.compose.ui.unit.IntSize
27 import androidx.compose.ui.util.fastForEach
28 import com.android.compose.animation.scene.transformation.AnchoredSize
29 import com.android.compose.animation.scene.transformation.AnchoredTranslate
30 import com.android.compose.animation.scene.transformation.DrawScale
31 import com.android.compose.animation.scene.transformation.EdgeTranslate
32 import com.android.compose.animation.scene.transformation.Fade
33 import com.android.compose.animation.scene.transformation.OverscrollTranslate
34 import com.android.compose.animation.scene.transformation.PropertyTransformation
35 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
36 import com.android.compose.animation.scene.transformation.ScaleSize
37 import com.android.compose.animation.scene.transformation.SharedElementTransformation
38 import com.android.compose.animation.scene.transformation.Transformation
39 import com.android.compose.animation.scene.transformation.Translate
40 
41 /** The transitions configuration of a [SceneTransitionLayout]. */
42 class SceneTransitions
43 internal constructor(
44     internal val defaultSwipeSpec: SpringSpec<Float>,
45     internal val transitionSpecs: List<TransitionSpecImpl>,
46     internal val overscrollSpecs: List<OverscrollSpecImpl>,
47     internal val interruptionHandler: InterruptionHandler,
48 ) {
49     private val transitionCache =
50         mutableMapOf<
51             SceneKey, MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>>
52         >()
53 
54     private val overscrollCache =
55         mutableMapOf<SceneKey, MutableMap<Orientation, OverscrollSpecImpl?>>()
56 
57     internal fun transitionSpec(
58         from: SceneKey,
59         to: SceneKey,
60         key: TransitionKey?,
61     ): TransitionSpecImpl {
62         return transitionCache
63             .getOrPut(from) { mutableMapOf() }
64             .getOrPut(to) { mutableMapOf() }
65             .getOrPut(key) { findSpec(from, to, key) }
66     }
67 
68     private fun findSpec(from: SceneKey, to: SceneKey, key: TransitionKey?): TransitionSpecImpl {
69         val spec = transition(from, to, key) { it.from == from && it.to == to }
70         if (spec != null) {
71             return spec
72         }
73 
74         val reversed = transition(from, to, key) { it.from == to && it.to == from }
75         if (reversed != null) {
76             return reversed.reversed()
77         }
78 
79         val relaxedSpec =
80             transition(from, to, key) {
81                 (it.from == from && it.to == null) || (it.to == to && it.from == null)
82             }
83         if (relaxedSpec != null) {
84             return relaxedSpec
85         }
86 
87         return transition(from, to, key) {
88                 (it.from == to && it.to == null) || (it.to == from && it.from == null)
89             }
90             ?.reversed()
91             ?: defaultTransition(from, to)
92     }
93 
94     private fun transition(
95         from: SceneKey,
96         to: SceneKey,
97         key: TransitionKey?,
98         filter: (TransitionSpecImpl) -> Boolean,
99     ): TransitionSpecImpl? {
100         var match: TransitionSpecImpl? = null
101         transitionSpecs.fastForEach { spec ->
102             if (spec.key == key && filter(spec)) {
103                 if (match != null) {
104                     error("Found multiple transition specs for transition $from => $to")
105                 }
106                 match = spec
107             }
108         }
109         return match
110     }
111 
112     private fun defaultTransition(from: SceneKey, to: SceneKey) =
113         TransitionSpecImpl(key = null, from, to, TransformationSpec.EmptyProvider)
114 
115     internal fun overscrollSpec(scene: SceneKey, orientation: Orientation): OverscrollSpecImpl? =
116         overscrollCache
117             .getOrPut(scene) { mutableMapOf() }
118             .getOrPut(orientation) { overscroll(scene, orientation) { it.scene == scene } }
119 
120     private fun overscroll(
121         scene: SceneKey,
122         orientation: Orientation,
123         filter: (OverscrollSpecImpl) -> Boolean,
124     ): OverscrollSpecImpl? {
125         var match: OverscrollSpecImpl? = null
126         overscrollSpecs.fastForEach { spec ->
127             if (spec.orientation == orientation && filter(spec)) {
128                 if (match != null) {
129                     error("Found multiple overscroll specs for overscroll $scene")
130                 }
131                 match = spec
132             }
133         }
134         return match
135     }
136 
137     companion object {
138         internal val DefaultSwipeSpec =
139             spring(
140                 stiffness = Spring.StiffnessMediumLow,
141                 visibilityThreshold = OffsetVisibilityThreshold,
142             )
143 
144         val Empty =
145             SceneTransitions(
146                 defaultSwipeSpec = DefaultSwipeSpec,
147                 transitionSpecs = emptyList(),
148                 overscrollSpecs = emptyList(),
149                 interruptionHandler = DefaultInterruptionHandler,
150             )
151     }
152 }
153 
154 /** The definition of a transition between [from] and [to]. */
155 interface TransitionSpec {
156     /** The key of this [TransitionSpec]. */
157     val key: TransitionKey?
158 
159     /**
160      * The scene we are transitioning from. If `null`, this spec can be used to animate from any
161      * scene.
162      */
163     val from: SceneKey?
164 
165     /**
166      * The scene we are transitioning to. If `null`, this spec can be used to animate from any
167      * scene.
168      */
169     val to: SceneKey?
170 
171     /**
172      * Return a reversed version of this [TransitionSpec] for a transition going from [to] to
173      * [from].
174      */
reversednull175     fun reversed(): TransitionSpec
176 
177     /**
178      * The [TransformationSpec] associated to this [TransitionSpec].
179      *
180      * Note that this is called once every a transition associated to this [TransitionSpec] is
181      * started.
182      */
183     fun transformationSpec(): TransformationSpec
184 }
185 
186 interface TransformationSpec {
187     /**
188      * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
189      * the transition is triggered (i.e. it is not gesture-based).
190      */
191     val progressSpec: AnimationSpec<Float>
192 
193     /**
194      * The [SpringSpec] used to animate the associated transition progress when the transition was
195      * started by a swipe and is now animating back to a scene because the user lifted their finger.
196      *
197      * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
198      */
199     val swipeSpec: SpringSpec<Float>?
200 
201     /**
202      * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
203      * [UserAction].
204      *
205      * If `null`, a default distance will be used that depends on the [UserAction] performed.
206      */
207     val distance: UserActionDistance?
208 
209     /** The list of [Transformation] applied to elements during this transition. */
210     val transformations: List<Transformation>
211 
212     companion object {
213         internal val Empty =
214             TransformationSpecImpl(
215                 progressSpec = snap(),
216                 swipeSpec = null,
217                 distance = null,
218                 transformations = emptyList(),
219             )
220         internal val EmptyProvider = { Empty }
221     }
222 }
223 
224 internal class TransitionSpecImpl(
225     override val key: TransitionKey?,
226     override val from: SceneKey?,
227     override val to: SceneKey?,
228     private val transformationSpec: () -> TransformationSpecImpl,
229 ) : TransitionSpec {
reversednull230     override fun reversed(): TransitionSpecImpl {
231         return TransitionSpecImpl(
232             key = key,
233             from = to,
234             to = from,
235             transformationSpec = {
236                 val reverse = transformationSpec.invoke()
237                 TransformationSpecImpl(
238                     progressSpec = reverse.progressSpec,
239                     swipeSpec = reverse.swipeSpec,
240                     distance = reverse.distance,
241                     transformations = reverse.transformations.map { it.reversed() }
242                 )
243             }
244         )
245     }
246 
transformationSpecnull247     override fun transformationSpec(): TransformationSpecImpl = this.transformationSpec.invoke()
248 }
249 
250 /** The definition of the overscroll behavior of the [scene]. */
251 interface OverscrollSpec {
252     /** The scene we are over scrolling. */
253     val scene: SceneKey
254 
255     /** The orientation of this [OverscrollSpec]. */
256     val orientation: Orientation
257 
258     /** The [TransformationSpec] associated to this [OverscrollSpec]. */
259     val transformationSpec: TransformationSpec
260 }
261 
262 internal class OverscrollSpecImpl(
263     override val scene: SceneKey,
264     override val orientation: Orientation,
265     override val transformationSpec: TransformationSpecImpl,
266 ) : OverscrollSpec
267 
268 /**
269  * An implementation of [TransformationSpec] that allows the quick retrieval of an element
270  * [ElementTransformations].
271  */
272 internal class TransformationSpecImpl(
273     override val progressSpec: AnimationSpec<Float>,
274     override val swipeSpec: SpringSpec<Float>?,
275     override val distance: UserActionDistance?,
276     override val transformations: List<Transformation>,
277 ) : TransformationSpec {
278     private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
279 
transformationsnull280     internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations {
281         return cache
282             .getOrPut(element) { mutableMapOf() }
283             .getOrPut(scene) { computeTransformations(element, scene) }
284     }
285 
286     /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
computeTransformationsnull287     private fun computeTransformations(
288         element: ElementKey,
289         scene: SceneKey,
290     ): ElementTransformations {
291         var shared: SharedElementTransformation? = null
292         var offset: PropertyTransformation<Offset>? = null
293         var size: PropertyTransformation<IntSize>? = null
294         var drawScale: PropertyTransformation<Scale>? = null
295         var alpha: PropertyTransformation<Float>? = null
296 
297         fun <T> onPropertyTransformation(
298             root: PropertyTransformation<T>,
299             current: PropertyTransformation<T> = root,
300         ) {
301             when (current) {
302                 is Translate,
303                 is OverscrollTranslate,
304                 is EdgeTranslate,
305                 is AnchoredTranslate -> {
306                     throwIfNotNull(offset, element, name = "offset")
307                     offset = root as PropertyTransformation<Offset>
308                 }
309                 is ScaleSize,
310                 is AnchoredSize -> {
311                     throwIfNotNull(size, element, name = "size")
312                     size = root as PropertyTransformation<IntSize>
313                 }
314                 is DrawScale -> {
315                     throwIfNotNull(drawScale, element, name = "drawScale")
316                     drawScale = root as PropertyTransformation<Scale>
317                 }
318                 is Fade -> {
319                     throwIfNotNull(alpha, element, name = "alpha")
320                     alpha = root as PropertyTransformation<Float>
321                 }
322                 is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
323             }
324         }
325 
326         transformations.fastForEach { transformation ->
327             if (!transformation.matcher.matches(element, scene)) {
328                 return@fastForEach
329             }
330 
331             when (transformation) {
332                 is SharedElementTransformation -> {
333                     throwIfNotNull(shared, element, name = "shared")
334                     shared = transformation
335                 }
336                 is PropertyTransformation<*> -> onPropertyTransformation(transformation)
337             }
338         }
339 
340         return ElementTransformations(shared, offset, size, drawScale, alpha)
341     }
342 
throwIfNotNullnull343     private fun throwIfNotNull(
344         previous: Transformation?,
345         element: ElementKey,
346         name: String,
347     ) {
348         if (previous != null) {
349             error("$element has multiple $name transformations")
350         }
351     }
352 }
353 
354 /** The transformations of an element during a transition. */
355 internal class ElementTransformations(
356     val shared: SharedElementTransformation?,
357     val offset: PropertyTransformation<Offset>?,
358     val size: PropertyTransformation<IntSize>?,
359     val drawScale: PropertyTransformation<Scale>?,
360     val alpha: PropertyTransformation<Float>?,
361 )
362