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.DurationBasedAnimationSpec
21 import androidx.compose.animation.core.Spring
22 import androidx.compose.animation.core.VectorConverter
23 import androidx.compose.animation.core.spring
24 import androidx.compose.ui.graphics.Shape
25 import androidx.compose.ui.unit.Dp
26 import com.android.compose.animation.scene.transformation.AnchoredSize
27 import com.android.compose.animation.scene.transformation.AnchoredTranslate
28 import com.android.compose.animation.scene.transformation.EdgeTranslate
29 import com.android.compose.animation.scene.transformation.Fade
30 import com.android.compose.animation.scene.transformation.PropertyTransformation
31 import com.android.compose.animation.scene.transformation.PunchHole
32 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
33 import com.android.compose.animation.scene.transformation.ScaleSize
34 import com.android.compose.animation.scene.transformation.Transformation
35 import com.android.compose.animation.scene.transformation.TransformationRange
36 import com.android.compose.animation.scene.transformation.Translate
37 
38 internal fun transitionsImpl(
39     builder: SceneTransitionsBuilder.() -> Unit,
40 ): SceneTransitions {
41     val impl = SceneTransitionsBuilderImpl().apply(builder)
42     return SceneTransitions(impl.transitionSpecs)
43 }
44 
45 private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
46     val transitionSpecs = mutableListOf<TransitionSpec>()
47 
48     override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
49         return transition(from = null, to = to, builder)
50     }
51 
52     override fun from(
53         from: SceneKey,
54         to: SceneKey?,
55         builder: TransitionBuilder.() -> Unit
56     ): TransitionSpec {
57         return transition(from = from, to = to, builder)
58     }
59 
60     private fun transition(
61         from: SceneKey?,
62         to: SceneKey?,
63         builder: TransitionBuilder.() -> Unit,
64     ): TransitionSpec {
65         val impl = TransitionBuilderImpl().apply(builder)
66         val spec =
67             TransitionSpec(
68                 from,
69                 to,
70                 impl.transformations,
71                 impl.spec,
72             )
73         transitionSpecs.add(spec)
74         return spec
75     }
76 }
77 
78 internal class TransitionBuilderImpl : TransitionBuilder {
79     val transformations = mutableListOf<Transformation>()
80     override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
81 
82     private var range: TransformationRange? = null
83     private val durationMillis: Int by lazy {
84         val spec = spec
85         if (spec !is DurationBasedAnimationSpec) {
86             error("timestampRange {} can only be used with a DurationBasedAnimationSpec")
87         }
88 
89         spec.vectorize(Float.VectorConverter).durationMillis
90     }
91 
92     override fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape) {
93         transformations.add(PunchHole(matcher, bounds, shape))
94     }
95 
96     override fun fractionRange(
97         start: Float?,
98         end: Float?,
99         builder: PropertyTransformationBuilder.() -> Unit
100     ) {
101         range = TransformationRange(start, end)
102         builder()
103         range = null
104     }
105 
106     override fun timestampRange(
107         startMillis: Int?,
108         endMillis: Int?,
109         builder: PropertyTransformationBuilder.() -> Unit
110     ) {
111         if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) {
112             error("invalid start value: startMillis=$startMillis durationMillis=$durationMillis")
113         }
114 
115         if (endMillis != null && (endMillis < 0 || endMillis > durationMillis)) {
116             error("invalid end value: endMillis=$startMillis durationMillis=$durationMillis")
117         }
118 
119         val start = startMillis?.let { it.toFloat() / durationMillis }
120         val end = endMillis?.let { it.toFloat() / durationMillis }
121         fractionRange(start, end, builder)
122     }
123 
124     private fun transformation(transformation: PropertyTransformation<*>) {
125         if (range != null) {
126             transformations.add(RangedPropertyTransformation(transformation, range!!))
127         } else {
128             transformations.add(transformation)
129         }
130     }
131 
132     override fun fade(matcher: ElementMatcher) {
133         transformation(Fade(matcher))
134     }
135 
136     override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) {
137         transformation(Translate(matcher, x, y))
138     }
139 
140     override fun translate(
141         matcher: ElementMatcher,
142         edge: Edge,
143         startsOutsideLayoutBounds: Boolean
144     ) {
145         transformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
146     }
147 
148     override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) {
149         transformation(AnchoredTranslate(matcher, anchor))
150     }
151 
152     override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) {
153         transformation(ScaleSize(matcher, width, height))
154     }
155 
156     override fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey) {
157         transformation(AnchoredSize(matcher, anchor))
158     }
159 }
160