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.ui.graphics.RectangleShape
21 import androidx.compose.ui.graphics.Shape
22 import androidx.compose.ui.unit.Dp
23 import androidx.compose.ui.unit.dp
24 
25 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
26 fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
27     return transitionsImpl(builder)
28 }
29 
30 @DslMarker annotation class TransitionDsl
31 
32 @TransitionDsl
33 interface SceneTransitionsBuilder {
34     /**
35      * Define the default animation to be played when transitioning [to] the specified scene, from
36      * any scene. For the animation specification to apply only when transitioning between two
37      * specific scenes, use [from] instead.
38      *
39      * @see from
40      */
41     fun to(
42         to: SceneKey,
43         builder: TransitionBuilder.() -> Unit = {},
44     ): TransitionSpec
45 
46     /**
47      * Define the animation to be played when transitioning [from] the specified scene. For the
48      * animation specification to apply only when transitioning between two specific scenes, pass
49      * the destination scene via the [to] argument.
50      *
51      * When looking up which transition should be used when animating from scene A to scene B, we
52      * pick the single transition matching one of these predicates (in order of importance):
53      * 1. from == A && to == B
54      * 2. to == A && from == B, which is then treated in reverse.
55      * 3. (from == A && to == null) || (from == null && to == B)
56      * 4. (from == B && to == null) || (from == null && to == A), which is then treated in reverse.
57      */
58     fun from(
59         from: SceneKey,
60         to: SceneKey? = null,
61         builder: TransitionBuilder.() -> Unit = {},
62     ): TransitionSpec
63 }
64 
65 @TransitionDsl
66 interface TransitionBuilder : PropertyTransformationBuilder {
67     /**
68      * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
69      * performing programmatic (not input pointer tracking) animations.
70      */
71     var spec: AnimationSpec<Float>
72 
73     /**
74      * Define a progress-based range for the transformations inside [builder].
75      *
76      * For instance, the following will fade `Foo` during the first half of the transition then it
77      * will translate it by 100.dp during the second half.
78      *
79      * ```
80      * fractionRange(end = 0.5f) { fade(Foo) }
81      * fractionRange(start = 0.5f) { translate(Foo, x = 100.dp) }
82      * ```
83      *
84      * @param start the start of the range, in the [0; 1] range.
85      * @param end the end of the range, in the [0; 1] range.
86      */
87     fun fractionRange(
88         start: Float? = null,
89         end: Float? = null,
90         builder: PropertyTransformationBuilder.() -> Unit,
91     )
92 
93     /**
94      * Define a timestamp-based range for the transformations inside [builder].
95      *
96      * For instance, the following will fade `Foo` during the first half of the transition then it
97      * will translate it by 100.dp during the second half.
98      *
99      * ```
100      * spec = tween(500)
101      * timestampRange(end = 250) { fade(Foo) }
102      * timestampRange(start = 250) { translate(Foo, x = 100.dp) }
103      * ```
104      *
105      * Important: [spec] must be a [androidx.compose.animation.core.DurationBasedAnimationSpec] if
106      * you call [timestampRange], otherwise this will throw. The spec duration will be used to
107      * transform this range into a [fractionRange].
108      *
109      * @param startMillis the start of the range, in the [0; spec.duration] range.
110      * @param endMillis the end of the range, in the [0; spec.duration] range.
111      */
112     fun timestampRange(
113         startMillis: Int? = null,
114         endMillis: Int? = null,
115         builder: PropertyTransformationBuilder.() -> Unit,
116     )
117 
118     /**
119      * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and
120      * using the given [shape].
121      *
122      * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
123      * This can be used to make content drawn below an opaque element visible. For example, if we
124      * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
125      * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
126      * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
127      * the result.
128      */
129     fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape)
130 }
131 
132 @TransitionDsl
133 interface PropertyTransformationBuilder {
134     /**
135      * Fade the element(s) matching [matcher]. This will automatically fade in or fade out if the
136      * element is entering or leaving the scene, respectively.
137      */
138     fun fade(matcher: ElementMatcher)
139 
140     /** Translate the element(s) matching [matcher] by ([x], [y]) dp. */
141     fun translate(matcher: ElementMatcher, x: Dp = 0.dp, y: Dp = 0.dp)
142 
143     /**
144      * Translate the element(s) matching [matcher] from/to the [edge] of the [SceneTransitionLayout]
145      * animating it.
146      *
147      * If [startsOutsideLayoutBounds] is `true`, then the element will start completely outside of
148      * the layout bounds (i.e. none of it will be visible at progress = 0f if the layout clips its
149      * content). If it is `false`, then the element will start aligned with the edge of the layout
150      * (i.e. it will be completely visible at progress = 0f).
151      */
152     fun translate(matcher: ElementMatcher, edge: Edge, startsOutsideLayoutBounds: Boolean = true)
153 
154     /**
155      * Translate the element(s) matching [matcher] by the same amount that [anchor] is translated
156      * during this transition.
157      *
158      * Note: This currently only works if [anchor] is a shared element of this transition.
159      *
160      * TODO(b/290184746): Also support anchors that are not shared but translated because of other
161      *   transformations, like an edge translation.
162      */
163     fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey)
164 
165     /**
166      * Scale the [width] and [height] of the element(s) matching [matcher]. Note that this scaling
167      * is done during layout, so it will potentially impact the size and position of other elements.
168      *
169      * TODO(b/290184746): Also provide a scaleDrawing() to scale an element at drawing time.
170      */
171     fun scaleSize(matcher: ElementMatcher, width: Float = 1f, height: Float = 1f)
172 
173     /**
174      * Scale the element(s) matching [matcher] so that it grows/shrinks to the same size as [anchor]
175      * .
176      *
177      * Note: This currently only works if [anchor] is a shared element of this transition.
178      */
179     fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey)
180 }
181 
182 /** An interface to match one or more elements. */
183 interface ElementMatcher {
184     /** Whether the element with key [key] matches this matcher. */
185     fun matches(key: ElementKey): Boolean
186 }
187 
188 /** The edge of a [SceneTransitionLayout]. */
189 enum class Edge {
190     Left,
191     Right,
192     Top,
193     Bottom,
194 }
195