1 /*
2  * 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 com.android.compose.animation.scene.transformation.AnchoredSize
24 import com.android.compose.animation.scene.transformation.AnchoredTranslate
25 import com.android.compose.animation.scene.transformation.EdgeTranslate
26 import com.android.compose.animation.scene.transformation.Fade
27 import com.android.compose.animation.scene.transformation.ModifierTransformation
28 import com.android.compose.animation.scene.transformation.PropertyTransformation
29 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
30 import com.android.compose.animation.scene.transformation.ScaleSize
31 import com.android.compose.animation.scene.transformation.Transformation
32 import com.android.compose.animation.scene.transformation.Translate
33 import com.android.compose.ui.util.fastForEach
34 import com.android.compose.ui.util.fastMap
35 
36 /** The transitions configuration of a [SceneTransitionLayout]. */
37 class SceneTransitions(
38     private val transitionSpecs: List<TransitionSpec>,
39 ) {
40     private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
41 
42     internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
43         return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
44     }
45 
46     private fun findSpec(from: SceneKey, to: SceneKey): TransitionSpec {
47         val spec = transition(from, to) { it.from == from && it.to == to }
48         if (spec != null) {
49             return spec
50         }
51 
52         val reversed = transition(from, to) { it.from == to && it.to == from }
53         if (reversed != null) {
54             return reversed.reverse()
55         }
56 
57         val relaxedSpec =
58             transition(from, to) {
59                 (it.from == from && it.to == null) || (it.to == to && it.from == null)
60             }
61         if (relaxedSpec != null) {
62             return relaxedSpec
63         }
64 
65         return transition(from, to) {
66                 (it.from == to && it.to == null) || (it.to == from && it.from == null)
67             }
68             ?.reverse()
69             ?: defaultTransition(from, to)
70     }
71 
72     private fun transition(
73         from: SceneKey,
74         to: SceneKey,
75         filter: (TransitionSpec) -> Boolean,
76     ): TransitionSpec? {
77         var match: TransitionSpec? = null
78         transitionSpecs.fastForEach { spec ->
79             if (filter(spec)) {
80                 if (match != null) {
81                     error("Found multiple transition specs for transition $from => $to")
82                 }
83                 match = spec
84             }
85         }
86         return match
87     }
88 
89     private fun defaultTransition(from: SceneKey, to: SceneKey) =
90         TransitionSpec(from, to, emptyList(), snap())
91 }
92 
93 /** The definition of a transition between [from] and [to]. */
94 data class TransitionSpec(
95     val from: SceneKey?,
96     val to: SceneKey?,
97     val transformations: List<Transformation>,
98     val spec: AnimationSpec<Float>,
99 ) {
100     private val cache = mutableMapOf<ElementKey, ElementTransformations>()
101 
102     internal fun reverse(): TransitionSpec {
103         return copy(
104             from = to,
105             to = from,
106             transformations = transformations.fastMap { it.reverse() },
107         )
108     }
109 
110     internal fun transformations(element: ElementKey): ElementTransformations {
111         return cache.getOrPut(element) { computeTransformations(element) }
112     }
113 
114     /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
115     private fun computeTransformations(element: ElementKey): ElementTransformations {
116         val modifier = mutableListOf<ModifierTransformation>()
117         var offset: PropertyTransformation<Offset>? = null
118         var size: PropertyTransformation<IntSize>? = null
119         var alpha: PropertyTransformation<Float>? = null
120 
121         fun <T> onPropertyTransformation(
122             root: PropertyTransformation<T>,
123             current: PropertyTransformation<T> = root,
124         ) {
125             when (current) {
126                 is Translate,
127                 is EdgeTranslate,
128                 is AnchoredTranslate -> {
129                     throwIfNotNull(offset, element, property = "offset")
130                     offset = root as PropertyTransformation<Offset>
131                 }
132                 is ScaleSize,
133                 is AnchoredSize -> {
134                     throwIfNotNull(size, element, property = "size")
135                     size = root as PropertyTransformation<IntSize>
136                 }
137                 is Fade -> {
138                     throwIfNotNull(alpha, element, property = "alpha")
139                     alpha = root as PropertyTransformation<Float>
140                 }
141                 is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
142             }
143         }
144 
145         transformations.fastForEach { transformation ->
146             if (!transformation.matcher.matches(element)) {
147                 return@fastForEach
148             }
149 
150             when (transformation) {
151                 is ModifierTransformation -> modifier.add(transformation)
152                 is PropertyTransformation<*> -> onPropertyTransformation(transformation)
153             }
154         }
155 
156         return ElementTransformations(modifier, offset, size, alpha)
157     }
158 
159     private fun throwIfNotNull(
160         previous: PropertyTransformation<*>?,
161         element: ElementKey,
162         property: String,
163     ) {
164         if (previous != null) {
165             error("$element has multiple transformations for its $property property")
166         }
167     }
168 }
169 
170 /** The transformations of an element during a transition. */
171 internal class ElementTransformations(
172     val modifier: List<ModifierTransformation>,
173     val offset: PropertyTransformation<Offset>?,
174     val size: PropertyTransformation<IntSize>?,
175     val alpha: PropertyTransformation<Float>?,
176 )
177