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