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.content.state.TransitionState
29 import com.android.compose.animation.scene.transformation.PropertyTransformation
30 import com.android.compose.animation.scene.transformation.SharedElementTransformation
31 import com.android.compose.animation.scene.transformation.TransformationMatcher
32 import com.android.compose.animation.scene.transformation.TransformationWithRange
33 
34 /** The transitions configuration of a [SceneTransitionLayout]. */
35 class SceneTransitions
36 internal constructor(
37     internal val defaultSwipeSpec: SpringSpec<Float>,
38     internal val transitionSpecs: List<TransitionSpecImpl>,
39     internal val overscrollSpecs: List<OverscrollSpecImpl>,
40     internal val interruptionHandler: InterruptionHandler,
41     internal val defaultProgressConverter: ProgressConverter,
42 ) {
43     private val transitionCache =
44         mutableMapOf<
45             ContentKey,
46             MutableMap<ContentKey, MutableMap<TransitionKey?, TransitionSpecImpl>>,
47         >()
48 
49     private val overscrollCache =
50         mutableMapOf<ContentKey, MutableMap<Orientation, OverscrollSpecImpl?>>()
51 
52     internal fun transitionSpec(
53         from: ContentKey,
54         to: ContentKey,
55         key: TransitionKey?,
56     ): TransitionSpecImpl {
57         return transitionCache
58             .getOrPut(from) { mutableMapOf() }
59             .getOrPut(to) { mutableMapOf() }
60             .getOrPut(key) { findSpec(from, to, key) }
61     }
62 
63     private fun findSpec(
64         from: ContentKey,
65         to: ContentKey,
66         key: TransitionKey?,
67     ): TransitionSpecImpl {
68         val spec = transition(from, to, key) { it.from == from && it.to == to }
69         if (spec != null) {
70             return spec
71         }
72 
73         val reversed = transition(from, to, key) { it.from == to && it.to == from }
74         if (reversed != null) {
75             return reversed.reversed()
76         }
77 
78         val relaxedSpec =
79             transition(from, to, key) {
80                 (it.from == from && it.to == null) || (it.to == to && it.from == null)
81             }
82         if (relaxedSpec != null) {
83             return relaxedSpec
84         }
85 
86         val relaxedReversed =
87             transition(from, to, key) {
88                 (it.from == to && it.to == null) || (it.to == from && it.from == null)
89             }
90         if (relaxedReversed != null) {
91             return relaxedReversed.reversed()
92         }
93 
94         return if (key != null) {
95             findSpec(from, to, null)
96         } else {
97             defaultTransition(from, to)
98         }
99     }
100 
101     private fun transition(
102         from: ContentKey,
103         to: ContentKey,
104         key: TransitionKey?,
105         filter: (TransitionSpecImpl) -> Boolean,
106     ): TransitionSpecImpl? {
107         var match: TransitionSpecImpl? = null
108         transitionSpecs.fastForEach { spec ->
109             if (spec.key == key && filter(spec)) {
110                 if (match != null) {
111                     error("Found multiple transition specs for transition $from => $to")
112                 }
113                 match = spec
114             }
115         }
116         return match
117     }
118 
119     private fun defaultTransition(from: ContentKey, to: ContentKey) =
120         TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider)
121 
122     internal fun overscrollSpec(scene: ContentKey, orientation: Orientation): OverscrollSpecImpl? =
123         overscrollCache
124             .getOrPut(scene) { mutableMapOf() }
125             .getOrPut(orientation) { overscroll(scene, orientation) { it.content == scene } }
126 
127     private fun overscroll(
128         scene: ContentKey,
129         orientation: Orientation,
130         filter: (OverscrollSpecImpl) -> Boolean,
131     ): OverscrollSpecImpl? {
132         var match: OverscrollSpecImpl? = null
133         overscrollSpecs.fastForEach { spec ->
134             if (spec.orientation == orientation && filter(spec)) {
135                 if (match != null) {
136                     error("Found multiple overscroll specs for overscroll $scene")
137                 }
138                 match = spec
139             }
140         }
141         return match
142     }
143 
144     companion object {
145         internal val DefaultSwipeSpec =
146             spring(
147                 stiffness = Spring.StiffnessMediumLow,
148                 dampingRatio = Spring.DampingRatioLowBouncy,
149                 visibilityThreshold = OffsetVisibilityThreshold,
150             )
151 
152         val Empty =
153             SceneTransitions(
154                 defaultSwipeSpec = DefaultSwipeSpec,
155                 transitionSpecs = emptyList(),
156                 overscrollSpecs = emptyList(),
157                 interruptionHandler = DefaultInterruptionHandler,
158                 defaultProgressConverter = ProgressConverter.Default,
159             )
160     }
161 }
162 
163 /** The definition of a transition between [from] and [to]. */
164 internal interface TransitionSpec {
165     /** The key of this [TransitionSpec]. */
166     val key: TransitionKey?
167 
168     /**
169      * The content we are transitioning from. If `null`, this spec can be used to animate from any
170      * content.
171      */
172     val from: ContentKey?
173 
174     /**
175      * The content we are transitioning to. If `null`, this spec can be used to animate from any
176      * content.
177      */
178     val to: ContentKey?
179 
180     /**
181      * Return a reversed version of this [TransitionSpec] for a transition going from [to] to
182      * [from].
183      */
reversednull184     fun reversed(): TransitionSpec
185 
186     /**
187      * The [TransformationSpec] associated to this [TransitionSpec] for the given [transition].
188      *
189      * Note that this is called once whenever a transition associated to this [TransitionSpec] is
190      * started.
191      */
192     fun transformationSpec(transition: TransitionState.Transition): TransformationSpec
193 
194     /**
195      * The preview [TransformationSpec] associated to this [TransitionSpec] for the given
196      * [transition].
197      *
198      * Note that this is called once whenever a transition associated to this [TransitionSpec] is
199      * started.
200      */
201     fun previewTransformationSpec(transition: TransitionState.Transition): TransformationSpec?
202 }
203 
204 internal interface TransformationSpec {
205     /**
206      * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
207      * the transition is triggered (i.e. it is not gesture-based).
208      */
209     val progressSpec: AnimationSpec<Float>
210 
211     /**
212      * The [SpringSpec] used to animate the associated transition progress when the transition was
213      * started by a swipe and is now animating back to a scene because the user lifted their finger.
214      *
215      * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
216      */
217     val swipeSpec: SpringSpec<Float>?
218 
219     /**
220      * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
221      * [UserAction].
222      *
223      * If `null`, a default distance will be used that depends on the [UserAction] performed.
224      */
225     val distance: UserActionDistance?
226 
227     /** The list of [TransformationMatcher] applied to elements during this transformation. */
228     val transformationMatchers: List<TransformationMatcher>
229 
230     companion object {
231         internal val Empty =
232             TransformationSpecImpl(
233                 progressSpec = snap(),
234                 swipeSpec = null,
235                 distance = null,
236                 transformationMatchers = emptyList(),
237             )
238         internal val EmptyProvider = { _: TransitionState.Transition -> Empty }
239     }
240 }
241 
242 internal class TransitionSpecImpl(
243     override val key: TransitionKey?,
244     override val from: ContentKey?,
245     override val to: ContentKey?,
246     private val previewTransformationSpec:
247         ((TransitionState.Transition) -> TransformationSpecImpl)? =
248         null,
249     private val reversePreviewTransformationSpec:
250         ((TransitionState.Transition) -> TransformationSpecImpl)? =
251         null,
252     private val transformationSpec: (TransitionState.Transition) -> TransformationSpecImpl,
253 ) : TransitionSpec {
reversednull254     override fun reversed(): TransitionSpecImpl {
255         return TransitionSpecImpl(
256             key = key,
257             from = to,
258             to = from,
259             previewTransformationSpec = reversePreviewTransformationSpec,
260             reversePreviewTransformationSpec = previewTransformationSpec,
261             transformationSpec = { transition ->
262                 val reverse = transformationSpec.invoke(transition)
263                 TransformationSpecImpl(
264                     progressSpec = reverse.progressSpec,
265                     swipeSpec = reverse.swipeSpec,
266                     distance = reverse.distance,
267                     transformationMatchers =
268                         reverse.transformationMatchers.map {
269                             TransformationMatcher(
270                                 matcher = it.matcher,
271                                 factory = it.factory,
272                                 range = it.range?.reversed(),
273                             )
274                         },
275                 )
276             },
277         )
278     }
279 
transformationSpecnull280     override fun transformationSpec(
281         transition: TransitionState.Transition
282     ): TransformationSpecImpl = transformationSpec.invoke(transition)
283 
284     override fun previewTransformationSpec(
285         transition: TransitionState.Transition
286     ): TransformationSpecImpl? = previewTransformationSpec?.invoke(transition)
287 }
288 
289 /** The definition of the overscroll behavior of the [content]. */
290 internal interface OverscrollSpec {
291     /** The scene we are over scrolling. */
292     val content: ContentKey
293 
294     /** The orientation of this [OverscrollSpec]. */
295     val orientation: Orientation
296 
297     /** The [TransformationSpec] associated to this [OverscrollSpec]. */
298     val transformationSpec: TransformationSpec
299 
300     /**
301      * Function that takes a linear overscroll progress value ranging from 0 to +/- infinity and
302      * outputs the desired **overscroll progress value**.
303      *
304      * When the progress value is:
305      * - 0, the user is not overscrolling.
306      * - 1, the user overscrolled by exactly the [OverscrollBuilder.distance].
307      * - Greater than 1, the user overscrolled more than the [OverscrollBuilder.distance].
308      */
309     val progressConverter: ProgressConverter?
310 }
311 
312 internal class OverscrollSpecImpl(
313     override val content: ContentKey,
314     override val orientation: Orientation,
315     override val transformationSpec: TransformationSpecImpl,
316     override val progressConverter: ProgressConverter?,
317 ) : OverscrollSpec
318 
319 /**
320  * An implementation of [TransformationSpec] that allows the quick retrieval of an element
321  * [ElementTransformations].
322  */
323 internal class TransformationSpecImpl(
324     override val progressSpec: AnimationSpec<Float>,
325     override val swipeSpec: SpringSpec<Float>?,
326     override val distance: UserActionDistance?,
327     override val transformationMatchers: List<TransformationMatcher>,
328 ) : TransformationSpec {
329     private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>()
330 
transformationsnull331     internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations {
332         return cache
333             .getOrPut(element) { mutableMapOf() }
334             .getOrPut(content) { computeTransformations(element, content) }
335     }
336 
337     /** Filter [transformationMatchers] to compute the [ElementTransformations] of [element]. */
computeTransformationsnull338     private fun computeTransformations(
339         element: ElementKey,
340         content: ContentKey,
341     ): ElementTransformations {
342         var shared: TransformationWithRange<SharedElementTransformation>? = null
343         var offset: TransformationWithRange<PropertyTransformation<Offset>>? = null
344         var size: TransformationWithRange<PropertyTransformation<IntSize>>? = null
345         var drawScale: TransformationWithRange<PropertyTransformation<Scale>>? = null
346         var alpha: TransformationWithRange<PropertyTransformation<Float>>? = null
347 
348         transformationMatchers.fastForEach { transformationMatcher ->
349             if (!transformationMatcher.matcher.matches(element, content)) {
350                 return@fastForEach
351             }
352 
353             val transformation = transformationMatcher.factory.create()
354             val property =
355                 when (transformation) {
356                     is SharedElementTransformation -> {
357                         throwIfNotNull(shared, element, name = "shared")
358                         shared =
359                             TransformationWithRange(transformation, transformationMatcher.range)
360                         return@fastForEach
361                     }
362                     is PropertyTransformation<*> -> transformation.property
363                 }
364 
365             when (property) {
366                 is PropertyTransformation.Property.Offset -> {
367                     throwIfNotNull(offset, element, name = "offset")
368                     offset =
369                         TransformationWithRange(
370                             transformation as PropertyTransformation<Offset>,
371                             transformationMatcher.range,
372                         )
373                 }
374                 is PropertyTransformation.Property.Size -> {
375                     throwIfNotNull(size, element, name = "size")
376                     size =
377                         TransformationWithRange(
378                             transformation as PropertyTransformation<IntSize>,
379                             transformationMatcher.range,
380                         )
381                 }
382                 is PropertyTransformation.Property.Scale -> {
383                     throwIfNotNull(drawScale, element, name = "drawScale")
384                     drawScale =
385                         TransformationWithRange(
386                             transformation as PropertyTransformation<Scale>,
387                             transformationMatcher.range,
388                         )
389                 }
390                 is PropertyTransformation.Property.Alpha -> {
391                     throwIfNotNull(alpha, element, name = "alpha")
392                     alpha =
393                         TransformationWithRange(
394                             transformation as PropertyTransformation<Float>,
395                             transformationMatcher.range,
396                         )
397                 }
398             }
399         }
400 
401         return ElementTransformations(shared, offset, size, drawScale, alpha)
402     }
403 
throwIfNotNullnull404     private fun throwIfNotNull(
405         previous: TransformationWithRange<*>?,
406         element: ElementKey,
407         name: String,
408     ) {
409         if (previous != null) {
410             error("$element has multiple $name transformations")
411         }
412     }
413 }
414 
415 /** The transformations of an element during a transition. */
416 internal class ElementTransformations(
417     val shared: TransformationWithRange<SharedElementTransformation>?,
418     val offset: TransformationWithRange<PropertyTransformation<Offset>>?,
419     val size: TransformationWithRange<PropertyTransformation<IntSize>>?,
420     val drawScale: TransformationWithRange<PropertyTransformation<Scale>>?,
421     val alpha: TransformationWithRange<PropertyTransformation<Float>>?,
422 )
423