• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Easing
21 import androidx.compose.animation.core.LinearEasing
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.unit.Dp
24 import androidx.compose.ui.unit.dp
25 import com.android.compose.animation.scene.content.state.TransitionState
26 import com.android.compose.animation.scene.transformation.Transformation
27 import com.android.internal.jank.Cuj.CujType
28 
29 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
transitionsnull30 fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
31     return transitionsImpl(builder)
32 }
33 
34 @DslMarker annotation class TransitionDsl
35 
36 @TransitionDsl
37 interface SceneTransitionsBuilder {
38     /**
39      * The [InterruptionHandler] used when transitions are interrupted. Defaults to
40      * [DefaultInterruptionHandler].
41      */
42     var interruptionHandler: InterruptionHandler
43 
44     /**
45      * Define the default animation to be played when transitioning [to] the specified content, from
46      * any content. For the animation specification to apply only when transitioning between two
47      * specific contents, use [from] instead.
48      *
49      * If [key] is not `null`, then this transition will only be used if the same key is specified
50      * when triggering the transition.
51      *
52      * Optionally, define a [preview] animation which will be played during the first stage of the
53      * transition, e.g. during the predictive back gesture. In case your transition should be
54      * reversible with the reverse animation having a preview as well, define a [reversePreview].
55      *
56      * @see from
57      */
tonull58     fun to(
59         to: ContentKey,
60         key: TransitionKey? = null,
61         @CujType cuj: Int? = null,
62         preview: (TransitionBuilder.() -> Unit)? = null,
63         reversePreview: (TransitionBuilder.() -> Unit)? = null,
64         builder: TransitionBuilder.() -> Unit = {},
65     )
66 
67     /**
68      * Define the animation to be played when transitioning [from] the specified content. For the
69      * animation specification to apply only when transitioning between two specific contents, pass
70      * the destination content via the [to] argument.
71      *
72      * When looking up which transition should be used when animating from content A to content B,
73      * we pick the single transition with the given [key] and matching one of these predicates (in
74      * order of importance):
75      * 1. from == A && to == B
76      * 2. to == A && from == B, which is then treated in reverse.
77      * 3. (from == A && to == null) || (from == null && to == B)
78      * 4. (from == B && to == null) || (from == null && to == A), which is then treated in reverse.
79      *
80      * Optionally, define a [preview] animation which will be played during the first stage of the
81      * transition, e.g. during the predictive back gesture. In case your transition should be
82      * reversible with the reverse animation having a preview as well, define a [reversePreview].
83      */
fromnull84     fun from(
85         from: ContentKey,
86         to: ContentKey? = null,
87         key: TransitionKey? = null,
88         @CujType cuj: Int? = null,
89         preview: (TransitionBuilder.() -> Unit)? = null,
90         reversePreview: (TransitionBuilder.() -> Unit)? = null,
91         builder: TransitionBuilder.() -> Unit = {},
92     )
93 }
94 
95 interface BaseTransitionBuilder : PropertyTransformationBuilder {
96     /**
97      * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
98      * [UserAction].
99      *
100      * If `null`, a default distance will be used that depends on the [UserAction] performed.
101      */
102     var distance: UserActionDistance?
103 
104     /**
105      * Define a progress-based range for the transformations inside [builder].
106      *
107      * For instance, the following will fade `Foo` during the first half of the transition then it
108      * will translate it by 100.dp during the second half.
109      *
110      * ```
111      * fractionRange(end = 0.5f) { fade(Foo) }
112      * fractionRange(start = 0.5f) { translate(Foo, x = 100.dp) }
113      * ```
114      *
115      * @param start the start of the range, in the [0; 1] range.
116      * @param end the end of the range, in the [0; 1] range.
117      */
fractionRangenull118     fun fractionRange(
119         start: Float? = null,
120         end: Float? = null,
121         easing: Easing = LinearEasing,
122         builder: PropertyTransformationBuilder.() -> Unit,
123     )
124 }
125 
126 @TransitionDsl
127 interface TransitionBuilder : BaseTransitionBuilder {
128     /** The [TransitionState.Transition] for which we currently compute the transformations. */
129     val transition: TransitionState.Transition
130 
131     /**
132      * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
133      * the transition is triggered (i.e. it is not gesture-based).
134      */
135     var spec: AnimationSpec<Float>?
136 
137     /** The CUJ associated to this transitions. */
138     @CujType var cuj: Int?
139 
140     /**
141      * Define a timestamp-based range for the transformations inside [builder].
142      *
143      * For instance, the following will fade `Foo` during the first half of the transition then it
144      * will translate it by 100.dp during the second half.
145      *
146      * ```
147      * spec = tween(500)
148      * timestampRange(end = 250) { fade(Foo) }
149      * timestampRange(start = 250) { translate(Foo, x = 100.dp) }
150      * ```
151      *
152      * Important: [spec] must be a [androidx.compose.animation.core.DurationBasedAnimationSpec] if
153      * you call [timestampRange], otherwise this will throw. The spec duration will be used to
154      * transform this range into a [fractionRange].
155      *
156      * @param startMillis the start of the range, in the [0; spec.duration] range.
157      * @param endMillis the end of the range, in the [0; spec.duration] range.
158      */
159     fun timestampRange(
160         startMillis: Int? = null,
161         endMillis: Int? = null,
162         easing: Easing = LinearEasing,
163         builder: PropertyTransformationBuilder.() -> Unit,
164     )
165 
166     /**
167      * Configure the shared transition when [matcher] is shared between two scenes.
168      *
169      * @param enabled whether the matched element(s) should actually be shared in this transition.
170      *   Defaults to true.
171      * @param elevateInContent the content in which we should elevate the element when it is shared,
172      *   drawing above all other composables of that content. If `null` (the default), we will
173      *   simply draw this element in its original location. If not `null`, it has to be either the
174      *   [fromContent][TransitionState.Transition.fromContent] or
175      *   [toContent][TransitionState.Transition.toContent] of the transition.
176      */
177     fun sharedElement(
178         matcher: ElementMatcher,
179         enabled: Boolean = true,
180         elevateInContent: ContentKey? = null,
181     )
182 
183     /**
184      * Adds the transformations in [builder] but in reversed order. This allows you to partially
185      * reuse the definition of the transition from scene `Foo` to scene `Bar` inside the definition
186      * of the transition from scene `Bar` to scene `Foo`.
187      */
188     fun reversed(builder: TransitionBuilder.() -> Unit)
189 }
190 
191 /**
192  * An interface to decide where we should draw shared Elements or compose MovableElements.
193  *
194  * @see DefaultElementContentPicker
195  * @see HighestZIndexContentPicker
196  * @see LowestZIndexContentPicker
197  * @see MovableElementContentPicker
198  */
199 interface ElementContentPicker {
200     /**
201      * Return the content in which [element] should be drawn (when using `Modifier.element(key)`) or
202      * composed (when using `MovableElement(key)`) during the given [transition].
203      *
204      * Important: For [MovableElements][ContentScope.MovableElement], this content picker will
205      * *always* be used during transitions to decide whether we should compose that element in a
206      * given content or not. Therefore, you should make sure that the returned [ContentKey] contains
207      * the movable element, otherwise that element will not be composed in any scene during the
208      * transition.
209      */
contentDuringTransitionnull210     fun contentDuringTransition(
211         element: ElementKey,
212         transition: TransitionState.Transition,
213         fromContentZIndex: Long,
214         toContentZIndex: Long,
215     ): ContentKey
216 
217     /**
218      * Return [transition.fromContent] if it is in [contents] and [transition.toContent] is not, or
219      * return [transition.toContent] if it is in [contents] and [transition.fromContent] is not. If
220      * neither [transition.toContent] and [transition.fromContent] are in [contents] or if both
221      * [transition.fromContent] and [transition.toContent] are in [contents], throw an exception.
222      *
223      * This function can be useful when computing the content in which a movable element should be
224      * composed.
225      */
226     fun pickSingleContentIn(
227         contents: Set<ContentKey>,
228         transition: TransitionState.Transition,
229         element: ElementKey,
230     ): ContentKey {
231         val fromContent = transition.fromContent
232         val toContent = transition.toContent
233         val fromContentInContents = contents.contains(fromContent)
234         val toContentInContents = contents.contains(toContent)
235 
236         if (fromContentInContents && toContentInContents) {
237             error(
238                 "Element $element can be in both $fromContent and $toContent. You should add a " +
239                     "special case for this transition before calling pickSingleSceneIn()."
240             )
241         }
242 
243         if (!fromContentInContents && !toContentInContents) {
244             error(
245                 "Element $element can be neither in $fromContent and $toContent. This either " +
246                     "means that you should add one of them in the scenes set passed to " +
247                     "pickSingleSceneIn(), or there is an internal error and this element was " +
248                     "composed when it shouldn't be."
249             )
250         }
251 
252         return if (fromContentInContents) {
253             fromContent
254         } else {
255             toContent
256         }
257     }
258 }
259 
260 /**
261  * An element picker on which we can query the set of contents (scenes or overlays) that contain the
262  * element. This is needed by [MovableElement], that needs to know at composition time on which of
263  * the candidate contents an element should be composed.
264  *
265  * @see DefaultElementContentPicker(contents)
266  * @see HighestZIndexContentPicker(contents)
267  * @see LowestZIndexContentPicker(contents)
268  * @see MovableElementContentPicker
269  */
270 interface StaticElementContentPicker : ElementContentPicker {
271     /** The exhaustive lists of contents that contain this element. */
272     val contents: Set<ContentKey>
273 }
274 
275 /**
276  * An [ElementContentPicker] that draws/composes elements in the content with the highest z-order.
277  */
278 object HighestZIndexContentPicker : ElementContentPicker {
contentDuringTransitionnull279     override fun contentDuringTransition(
280         element: ElementKey,
281         transition: TransitionState.Transition,
282         fromContentZIndex: Long,
283         toContentZIndex: Long,
284     ): ContentKey {
285         return if (fromContentZIndex > toContentZIndex) {
286             transition.fromContent
287         } else {
288             transition.toContent
289         }
290     }
291 
292     /**
293      * Return a [StaticElementContentPicker] that behaves like [HighestZIndexContentPicker] and can
294      * be used by [MovableElement].
295      */
invokenull296     operator fun invoke(contents: Set<ContentKey>): StaticElementContentPicker {
297         return object : StaticElementContentPicker {
298             override val contents: Set<ContentKey> = contents
299 
300             override fun contentDuringTransition(
301                 element: ElementKey,
302                 transition: TransitionState.Transition,
303                 fromContentZIndex: Long,
304                 toContentZIndex: Long,
305             ): ContentKey {
306                 return HighestZIndexContentPicker.contentDuringTransition(
307                     element,
308                     transition,
309                     fromContentZIndex,
310                     toContentZIndex,
311                 )
312             }
313         }
314     }
315 }
316 
317 /**
318  * An [ElementContentPicker] that draws/composes elements in the content with the lowest z-order.
319  */
320 object LowestZIndexContentPicker : ElementContentPicker {
contentDuringTransitionnull321     override fun contentDuringTransition(
322         element: ElementKey,
323         transition: TransitionState.Transition,
324         fromContentZIndex: Long,
325         toContentZIndex: Long,
326     ): ContentKey {
327         return if (fromContentZIndex < toContentZIndex) {
328             transition.fromContent
329         } else {
330             transition.toContent
331         }
332     }
333 
334     /**
335      * Return a [StaticElementContentPicker] that behaves like [LowestZIndexContentPicker] and can
336      * be used by [MovableElement].
337      */
invokenull338     operator fun invoke(contents: Set<ContentKey>): StaticElementContentPicker {
339         return object : StaticElementContentPicker {
340             override val contents: Set<ContentKey> = contents
341 
342             override fun contentDuringTransition(
343                 element: ElementKey,
344                 transition: TransitionState.Transition,
345                 fromContentZIndex: Long,
346                 toContentZIndex: Long,
347             ): ContentKey {
348                 return LowestZIndexContentPicker.contentDuringTransition(
349                     element,
350                     transition,
351                     fromContentZIndex,
352                     toContentZIndex,
353                 )
354             }
355         }
356     }
357 }
358 
359 /**
360  * An [ElementContentPicker] that draws/composes elements in the content we are transitioning to,
361  * iff that content is in [contents].
362  *
363  * This picker can be useful for movable elements whose content size depends on its content (because
364  * it wraps it) in at least one scene. That way, the target size of the MovableElement will be
365  * computed in the scene we are going to and, given that this element was probably already composed
366  * in the scene we are going from before starting the transition, the interpolated size of the
367  * movable element during the transition should be correct.
368  *
369  * The downside of this picker is that the zIndex of the element when going from scene A to scene B
370  * is not the same as when going from scene B to scene A, so it's not usable in situations where
371  * z-ordering during the transition matters.
372  */
373 class MovableElementContentPicker(override val contents: Set<ContentKey>) :
374     StaticElementContentPicker {
contentDuringTransitionnull375     override fun contentDuringTransition(
376         element: ElementKey,
377         transition: TransitionState.Transition,
378         fromContentZIndex: Long,
379         toContentZIndex: Long,
380     ): ContentKey {
381         return when {
382             transition.toContent in contents -> transition.toContent
383             else -> {
384                 check(transition.fromContent in contents) {
385                     "Neither ${transition.fromContent} nor ${transition.toContent} are in " +
386                         "contents. This transition should not have been used for this element."
387                 }
388                 transition.fromContent
389             }
390         }
391     }
392 }
393 
394 /** The default [ElementContentPicker]. */
395 val DefaultElementContentPicker = HighestZIndexContentPicker
396 
397 /** The [DefaultElementContentPicker] that can be used for [MovableElement]s. */
DefaultElementContentPickernull398 fun DefaultElementContentPicker(contents: Set<ContentKey>): StaticElementContentPicker {
399     return HighestZIndexContentPicker(contents)
400 }
401 
402 @TransitionDsl
403 interface PropertyTransformationBuilder {
404     /**
405      * Fade the element(s) matching [matcher]. This will automatically fade in or fade out if the
406      * element is entering or leaving the scene, respectively.
407      */
fadenull408     fun fade(matcher: ElementMatcher)
409 
410     /** Translate the element(s) matching [matcher] by ([x], [y]) dp. */
411     fun translate(matcher: ElementMatcher, x: Dp = 0.dp, y: Dp = 0.dp)
412 
413     /**
414      * Translate the element(s) matching [matcher] from/to the [edge] of the [SceneTransitionLayout]
415      * animating it.
416      *
417      * If [startsOutsideLayoutBounds] is `true`, then the element will start completely outside of
418      * the layout bounds (i.e. none of it will be visible at progress = 0f if the layout clips its
419      * content). If it is `false`, then the element will start aligned with the edge of the layout
420      * (i.e. it will be completely visible at progress = 0f).
421      */
422     fun translate(matcher: ElementMatcher, edge: Edge, startsOutsideLayoutBounds: Boolean = true)
423 
424     /**
425      * Translate the element(s) matching [matcher] by the same amount that [anchor] is translated
426      * during this transition.
427      *
428      * Note: This currently only works if [anchor] is a shared element of this transition.
429      *
430      * TODO(b/290184746): Also support anchors that are not shared but translated because of other
431      *   transformations, like an edge translation.
432      */
433     fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey)
434 
435     /**
436      * Scale the [width] and [height] of the element(s) matching [matcher]. Note that this scaling
437      * is done during layout, so it will potentially impact the size and position of other elements.
438      */
439     fun scaleSize(matcher: ElementMatcher, width: Float = 1f, height: Float = 1f)
440 
441     /**
442      * Scale the drawing with [scaleX] and [scaleY] of the element(s) matching [matcher]. Note this
443      * will only scale the draw inside of an element, therefore it won't impact layout of elements
444      * around it.
445      */
446     fun scaleDraw(
447         matcher: ElementMatcher,
448         scaleX: Float = 1f,
449         scaleY: Float = 1f,
450         pivot: Offset = Offset.Unspecified,
451     )
452 
453     /**
454      * Scale the element(s) matching [matcher] so that it grows/shrinks to the same size as
455      * [anchor].
456      *
457      * Note: This currently only works if [anchor] is a shared element of this transition.
458      */
459     fun anchoredSize(
460         matcher: ElementMatcher,
461         anchor: ElementKey,
462         anchorWidth: Boolean = true,
463         anchorHeight: Boolean = true,
464     )
465 
466     /** Apply a [transformation] to the element(s) matching [matcher]. */
467     fun transformation(matcher: ElementMatcher, transformation: Transformation.Factory)
468 }
469