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]. */
transitionsnull26 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 */
tonull41 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 */
fromnull58 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 */
fractionRangenull87 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. */
matchesnull185 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