1 /*
2  * Copyright 2020 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 @file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalAnimationApi::class)
18 
19 package androidx.compose.animation
20 
21 import androidx.compose.animation.core.AnimationVector2D
22 import androidx.compose.animation.core.FiniteAnimationSpec
23 import androidx.compose.animation.core.Spring
24 import androidx.compose.animation.core.Transition
25 import androidx.compose.animation.core.TwoWayConverter
26 import androidx.compose.animation.core.VectorConverter
27 import androidx.compose.animation.core.VisibilityThreshold
28 import androidx.compose.animation.core.createDeferredAnimation
29 import androidx.compose.animation.core.spring
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.Immutable
32 import androidx.compose.runtime.Stable
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.mutableStateOf
35 import androidx.compose.runtime.remember
36 import androidx.compose.runtime.setValue
37 import androidx.compose.ui.Alignment
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.graphics.GraphicsLayerScope
40 import androidx.compose.ui.graphics.TransformOrigin
41 import androidx.compose.ui.graphics.graphicsLayer
42 import androidx.compose.ui.layout.ContentScale
43 import androidx.compose.ui.layout.Measurable
44 import androidx.compose.ui.layout.MeasureResult
45 import androidx.compose.ui.layout.MeasureScope
46 import androidx.compose.ui.node.ModifierNodeElement
47 import androidx.compose.ui.platform.InspectorInfo
48 import androidx.compose.ui.unit.Constraints
49 import androidx.compose.ui.unit.IntOffset
50 import androidx.compose.ui.unit.IntSize
51 import androidx.compose.ui.unit.LayoutDirection
52 import androidx.compose.ui.unit.constrain
53 
54 @RequiresOptIn(message = "This is an experimental animation API.")
55 @Target(
56     AnnotationTarget.CLASS,
57     AnnotationTarget.FUNCTION,
58     AnnotationTarget.PROPERTY,
59     AnnotationTarget.FIELD,
60     AnnotationTarget.PROPERTY_GETTER,
61 )
62 @Retention(AnnotationRetention.BINARY)
63 public annotation class ExperimentalAnimationApi
64 
65 /**
66  * [EnterTransition] defines how an [AnimatedVisibility] Composable appears on screen as it becomes
67  * visible. The 4 categories of EnterTransitions available are:
68  * 1. fade: [fadeIn]
69  * 2. scale: [scaleIn]
70  * 3. slide: [slideIn], [slideInHorizontally], [slideInVertically]
71  * 4. expand: [expandIn], [expandHorizontally], [expandVertically]
72  *
73  * [EnterTransition.None] can be used when no enter transition is desired. Different
74  * [EnterTransition]s can be combined using plus operator, for example:
75  *
76  * @sample androidx.compose.animation.samples.SlideTransition
77  *
78  * __Note__: [fadeIn], [scaleIn] and [slideIn] do not affect the size of the [AnimatedVisibility]
79  * composable. In contrast, [expandIn] will grow the clip bounds to reveal the whole content. This
80  * will automatically animate other layouts out of the way, very much like [animateContentSize].
81  *
82  * @see fadeIn
83  * @see scaleIn
84  * @see slideIn
85  * @see slideInHorizontally
86  * @see slideInVertically
87  * @see expandIn
88  * @see expandHorizontally
89  * @see expandVertically
90  * @see AnimatedVisibility
91  */
92 @Immutable
93 public sealed class EnterTransition {
94     internal abstract val data: TransitionData
95 
96     /**
97      * Combines different enter transitions. The order of the [EnterTransition]s being combined does
98      * not matter, as these [EnterTransition]s will start simultaneously. The order of applying
99      * transforms from these enter transitions (if defined) is: alpha and scale first, shrink or
100      * expand, then slide.
101      *
102      * @sample androidx.compose.animation.samples.FullyLoadedTransition
103      * @param enter another [EnterTransition] to be combined
104      */
105     @Stable
plusnull106     public operator fun plus(enter: EnterTransition): EnterTransition {
107         return EnterTransitionImpl(
108             TransitionData(
109                 fade = enter.data.fade ?: data.fade,
110                 slide = enter.data.slide ?: data.slide,
111                 changeSize = enter.data.changeSize ?: data.changeSize,
112                 scale = enter.data.scale ?: data.scale,
113                 // `enter` after plus operator to prioritize its values on the map
114                 effectsMap = data.effectsMap + enter.data.effectsMap
115             )
116         )
117     }
118 
toStringnull119     override fun toString(): String =
120         if (this == None) {
121             "EnterTransition.None"
122         } else {
<lambda>null123             data.run {
124                 "EnterTransition: \n" +
125                     "Fade - " +
126                     fade?.toString() +
127                     ",\nSlide - " +
128                     slide?.toString() +
129                     ",\nShrink - " +
130                     changeSize?.toString() +
131                     ",\nScale - " +
132                     scale?.toString()
133             }
134         }
135 
equalsnull136     override fun equals(other: Any?): Boolean {
137         return other is EnterTransition && other.data == data
138     }
139 
hashCodenull140     override fun hashCode(): Int = data.hashCode()
141 
142     public companion object {
143         /**
144          * This can be used when no enter transition is desired. It can be useful in cases where
145          * there are other forms of enter animation defined indirectly for an [AnimatedVisibility].
146          * e.g.The children of the [AnimatedVisibility] have all defined their own
147          * [EnterTransition], or when the parent is fading in, etc.
148          *
149          * @see [ExitTransition.None]
150          */
151         public val None: EnterTransition = EnterTransitionImpl(TransitionData())
152     }
153 }
154 
155 /**
156  * [ExitTransition] defines how an [AnimatedVisibility] Composable disappears on screen as it
157  * becomes not visible. The 4 categories of [ExitTransition] available are:
158  * 1. fade: [fadeOut]
159  * 2. scale: [scaleOut]
160  * 3. slide: [slideOut], [slideOutHorizontally], [slideOutVertically]
161  * 4. shrink: [shrinkOut], [shrinkHorizontally], [shrinkVertically]
162  *
163  * [ExitTransition.None] can be used when no exit transition is desired. Different [ExitTransition]s
164  * can be combined using plus operator, for example:
165  *
166  * @sample androidx.compose.animation.samples.SlideTransition
167  *
168  * __Note__: [fadeOut] and [slideOut] do not affect the size of the [AnimatedVisibility] composable.
169  * In contrast, [shrinkOut] (and [shrinkHorizontally], [shrinkVertically]) will shrink the clip
170  * bounds to reveal less and less of the content. This will automatically animate other layouts to
171  * fill in the space, very much like [animateContentSize].
172  *
173  * @see fadeOut
174  * @see scaleOut
175  * @see slideOut
176  * @see slideOutHorizontally
177  * @see slideOutVertically
178  * @see shrinkOut
179  * @see shrinkHorizontally
180  * @see shrinkVertically
181  * @see AnimatedVisibility
182  */
183 @Immutable
184 public sealed class ExitTransition {
185     internal abstract val data: TransitionData
186 
187     /**
188      * Combines different exit transitions. The order of the [ExitTransition]s being combined does
189      * not matter, as these [ExitTransition]s will start simultaneously. The order of applying
190      * transforms from these exit transitions (if defined) is: alpha and scale first, shrink or
191      * expand, then slide.
192      *
193      * @sample androidx.compose.animation.samples.FullyLoadedTransition
194      * @param exit another [ExitTransition] to be combined.
195      */
196     @Stable
plusnull197     public operator fun plus(exit: ExitTransition): ExitTransition {
198         return ExitTransitionImpl(
199             TransitionData(
200                 fade = exit.data.fade ?: data.fade,
201                 slide = exit.data.slide ?: data.slide,
202                 changeSize = exit.data.changeSize ?: data.changeSize,
203                 scale = exit.data.scale ?: data.scale,
204                 hold = exit.data.hold || data.hold,
205                 // `exit` after plus operator to prioritize its values on the map
206                 effectsMap = data.effectsMap + exit.data.effectsMap
207             )
208         )
209     }
210 
equalsnull211     override fun equals(other: Any?): Boolean {
212         return other is ExitTransition && other.data == data
213     }
214 
toStringnull215     override fun toString(): String =
216         when (this) {
217             None -> "ExitTransition.None"
218             KeepUntilTransitionsFinished -> "ExitTransition.KeepUntilTransitionsFinished"
219             else ->
220                 data.run {
221                     "ExitTransition: \n" +
222                         "Fade - " +
223                         fade?.toString() +
224                         ",\nSlide - " +
225                         slide?.toString() +
226                         ",\nShrink - " +
227                         changeSize?.toString() +
228                         ",\nScale - " +
229                         scale?.toString() +
230                         ",\nKeepUntilTransitionsFinished - " +
231                         hold
232                 }
233         }
234 
hashCodenull235     override fun hashCode(): Int = data.hashCode()
236 
237     public companion object {
238         /**
239          * This can be used when no built-in [ExitTransition] (i.e. fade/slide, etc) is desired for
240          * the [AnimatedVisibility], but rather the children are defining their own exit animation
241          * using the [Transition] scope.
242          *
243          * __Note:__ If [None] is used, and nothing is animating in the Transition<EnterExitState>
244          * scope that [AnimatedVisibility] provided, the content will be removed from
245          * [AnimatedVisibility] right away.
246          *
247          * @sample androidx.compose.animation.samples.AVScopeAnimateEnterExit
248          */
249         public val None: ExitTransition = ExitTransitionImpl(TransitionData())
250 
251         /**
252          * Keep this type of exit transition internal and only expose it in AnimatedContent, as
253          * holding only makes sense when there's enter and exit at the same time. In other words,
254          * when dealing with one set of content entering OR exiting, such as AnimatedVisibility,
255          * holding would not be meaningful.
256          */
257         internal val KeepUntilTransitionsFinished: ExitTransition =
258             ExitTransitionImpl(TransitionData(hold = true))
259     }
260 }
261 
262 internal sealed class TransitionEffect {
263     internal abstract val key: TransitionEffectKey<*>
264 }
265 
266 internal interface TransitionEffectKey<E : TransitionEffect>
267 
268 internal data class ContentScaleTransitionEffect(
269     val contentScale: ContentScale,
270     val alignment: Alignment,
271 ) : TransitionEffect() {
272     companion object Key : TransitionEffectKey<ContentScaleTransitionEffect>
273 
274     override val key: TransitionEffectKey<*>
275         get() = Key
276 }
277 
withEffectnull278 internal infix fun EnterTransition.withEffect(effect: TransitionEffect): EnterTransition =
279     EnterTransitionImpl(TransitionData(effectsMap = mapOf(effect.key to effect)))
280 
281 internal infix fun ExitTransition.withEffect(effect: TransitionEffect): ExitTransition =
282     ExitTransitionImpl(TransitionData(effectsMap = mapOf(effect.key to effect)))
283 
284 /**
285  * This fades in the content of the transition, from the specified starting alpha (i.e.
286  * [initialAlpha]) to 1f, using the supplied [animationSpec]. [initialAlpha] defaults to 0f, and
287  * [spring] is used by default.
288  *
289  * @sample androidx.compose.animation.samples.FadeTransition
290  * @param animationSpec the [FiniteAnimationSpec] for this animation, [spring] by default
291  * @param initialAlpha the starting alpha of the enter transition, 0f by default
292  */
293 @Stable
294 public fun fadeIn(
295     animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
296     initialAlpha: Float = 0f
297 ): EnterTransition {
298     return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
299 }
300 
301 /**
302  * This fades out the content of the transition, from full opacity to the specified target alpha
303  * (i.e. [targetAlpha]), using the supplied [animationSpec]. By default, the content will be faded
304  * out to fully transparent (i.e. [targetAlpha] defaults to 0), and [animationSpec] uses [spring] by
305  * default.
306  *
307  * @sample androidx.compose.animation.samples.FadeTransition
308  * @param animationSpec the [FiniteAnimationSpec] for this animation, [spring] by default
309  * @param targetAlpha the target alpha of the exit transition, 0f by default
310  */
311 @Stable
fadeOutnull312 public fun fadeOut(
313     animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
314     targetAlpha: Float = 0f,
315 ): ExitTransition {
316     return ExitTransitionImpl(TransitionData(fade = Fade(targetAlpha, animationSpec)))
317 }
318 
319 /**
320  * This slides in the content of the transition, from a starting offset defined in [initialOffset]
321  * to `IntOffset(0, 0)`. The direction of the slide can be controlled by configuring the
322  * [initialOffset]. A positive x value means sliding from right to left, whereas a negative x value
323  * will slide the content to the right. Similarly positive and negative y values correspond to
324  * sliding up and down, respectively.
325  *
326  * If the sliding is only desired horizontally or vertically, instead of along both axis, consider
327  * using [slideInHorizontally] or [slideInVertically].
328  *
329  * [initialOffset] is a lambda that takes the full size of the content and returns an offset. This
330  * allows the offset to be defined proportional to the full size, or as an absolute value.
331  *
332  * @sample androidx.compose.animation.samples.SlideInOutSample
333  * @param animationSpec the animation used for the slide-in, [spring] by default.
334  * @param initialOffset a lambda that takes the full size of the content and returns the initial
335  *   offset for the slide-in
336  */
337 @Stable
slideInnull338 public fun slideIn(
339     animationSpec: FiniteAnimationSpec<IntOffset> =
340         spring(
341             stiffness = Spring.StiffnessMediumLow,
342             visibilityThreshold = IntOffset.VisibilityThreshold
343         ),
344     initialOffset: (fullSize: IntSize) -> IntOffset,
345 ): EnterTransition {
346     return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
347 }
348 
349 /**
350  * This slides out the content of the transition, from an offset of `IntOffset(0, 0)` to the target
351  * offset defined in [targetOffset]. The direction of the slide can be controlled by configuring the
352  * [targetOffset]. A positive x value means sliding from left to right, whereas a negative x value
353  * would slide the content from right to left. Similarly, positive and negative y values correspond
354  * to sliding down and up, respectively.
355  *
356  * If the sliding is only desired horizontally or vertically, instead of along both axis, consider
357  * using [slideOutHorizontally] or [slideOutVertically].
358  *
359  * [targetOffset] is a lambda that takes the full size of the content and returns an offset. This
360  * allows the offset to be defined proportional to the full size, or as an absolute value.
361  *
362  * @sample androidx.compose.animation.samples.SlideInOutSample
363  * @param animationSpec the animation used for the slide-out, [spring] by default.
364  * @param targetOffset a lambda that takes the full size of the content and returns the target
365  *   offset for the slide-out
366  */
367 @Stable
slideOutnull368 public fun slideOut(
369     animationSpec: FiniteAnimationSpec<IntOffset> =
370         spring(
371             stiffness = Spring.StiffnessMediumLow,
372             visibilityThreshold = IntOffset.VisibilityThreshold
373         ),
374     targetOffset: (fullSize: IntSize) -> IntOffset,
375 ): ExitTransition {
376     return ExitTransitionImpl(TransitionData(slide = Slide(targetOffset, animationSpec)))
377 }
378 
379 /**
380  * This scales the content as it appears, from an initial scale (defined in [initialScale]) to 1f.
381  * [transformOrigin] defines the pivot point in terms of fraction of the overall size.
382  * [TransformOrigin.Center] by default. [scaleIn] can be used in combination with any other type of
383  * [EnterTransition] using the plus operator (e.g. `scaleIn() + slideInHorizontally()`)
384  *
385  * Note: Scale is applied __before__ slide. This means when using [slideIn]/[slideOut] with
386  * [scaleIn]/[scaleOut], the amount of scaling needs to be taken into account when sliding.
387  *
388  * The scaling will change the visual of the content, but will __not__ affect the layout size.
389  * [scaleIn] can be combined with [expandIn]/[expandHorizontally]/[expandVertically] to coordinate
390  * layout size change while scaling. For example:
391  *
392  * @sample androidx.compose.animation.samples.ScaledEnterExit
393  * @param animationSpec the animation used for the scale-out, [spring] by default.
394  * @param initialScale the initial scale for the enter transition, 0 by default.
395  * @param transformOrigin the pivot point in terms of fraction of the overall size. By default it's
396  *   [TransformOrigin.Center].
397  */
398 @Stable
scaleInnull399 public fun scaleIn(
400     animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
401     initialScale: Float = 0f,
402     transformOrigin: TransformOrigin = TransformOrigin.Center,
403 ): EnterTransition {
404     return EnterTransitionImpl(
405         TransitionData(scale = Scale(initialScale, transformOrigin, animationSpec))
406     )
407 }
408 
409 /**
410  * This scales the content of the exit transition, from 1f to the target scale defined in
411  * [targetScale]. [transformOrigin] defines the pivot point in terms of fraction of the overall
412  * size. By default it's [TransformOrigin.Center]. [scaleOut] can be used in combination with any
413  * other type of [ExitTransition] using the plus operator (e.g. `scaleOut() + fadeOut()`)
414  *
415  * Note: Scale is applied __before__ slide. This means when using [slideIn]/[slideOut] with
416  * [scaleIn]/[scaleOut], the amount of scaling needs to be taken into account when sliding.
417  *
418  * The scaling will change the visual of the content, but will __not__ affect the layout size.
419  * [scaleOut] can be combined with [shrinkOut]/[shrinkHorizontally]/[shrinkVertically] for
420  * coordinated layout size change animation. For example:
421  *
422  * @sample androidx.compose.animation.samples.ScaledEnterExit
423  * @param animationSpec the animation used for the slide-out, [spring] by default.
424  * @param targetScale the target scale for the exit transition, 0 by default.
425  * @param transformOrigin the pivot point in terms of fraction of the overall size. By default it's
426  *   [TransformOrigin.Center].
427  */
428 @Stable
scaleOutnull429 public fun scaleOut(
430     animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
431     targetScale: Float = 0f,
432     transformOrigin: TransformOrigin = TransformOrigin.Center
433 ): ExitTransition {
434     return ExitTransitionImpl(
435         TransitionData(scale = Scale(targetScale, transformOrigin, animationSpec))
436     )
437 }
438 
439 /**
440  * This expands the clip bounds of the appearing content from the size returned from [initialSize]
441  * to the full size. [expandFrom] controls which part of the content gets revealed first. By
442  * default, the clip bounds animates from `IntSize(0, 0)` to full size, starting from revealing the
443  * bottom right corner (or bottom left corner in RTL layouts) of the content, to fully revealing the
444  * entire content as the size expands.
445  *
446  * __Note__: [expandIn] animates the bounds of the content. This bounds change will also result in
447  * the animation of other layouts that are dependent on this size.
448  *
449  * [initialSize] is a lambda that takes the full size of the content and returns an initial size of
450  * the bounds of the content. This allows not only absolute size, but also an initial size that is
451  * proportional to the content size.
452  *
453  * [clip] defines whether the content outside of the animated bounds should be clipped. By default,
454  * clip is set to true, which only shows content in the animated bounds.
455  *
456  * For expanding only horizontally or vertically, consider [expandHorizontally], [expandVertically].
457  *
458  * @sample androidx.compose.animation.samples.ExpandInShrinkOutSample
459  * @param animationSpec the animation used for the expanding animation, [spring] by default.
460  * @param expandFrom the starting point of the expanding bounds, [Alignment.BottomEnd] by default.
461  * @param clip whether the content outside of the animated bounds should be clipped, true by default
462  * @param initialSize the start size of the expanding bounds, returning `IntSize(0, 0)` by default.
463  */
464 @Stable
expandInnull465 public fun expandIn(
466     animationSpec: FiniteAnimationSpec<IntSize> =
467         spring(
468             stiffness = Spring.StiffnessMediumLow,
469             visibilityThreshold = IntSize.VisibilityThreshold
470         ),
471     expandFrom: Alignment = Alignment.BottomEnd,
472     clip: Boolean = true,
473     initialSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
474 ): EnterTransition {
475     return EnterTransitionImpl(
476         TransitionData(changeSize = ChangeSize(expandFrom, initialSize, animationSpec, clip))
477     )
478 }
479 
480 /**
481  * This shrinks the clip bounds of the disappearing content from the full size to the size returned
482  * from [targetSize]. [shrinkTowards] controls the direction of the bounds shrink animation. By
483  * default, the clip bounds animates from full size to `IntSize(0, 0)`, shrinking towards the the
484  * bottom right corner (or bottom left corner in RTL layouts) of the content.
485  *
486  * __Note__: [shrinkOut] animates the bounds of the content. This bounds change will also result in
487  * the animation of other layouts that are dependent on this size.
488  *
489  * [targetSize] is a lambda that takes the full size of the content and returns a target size of the
490  * bounds of the content. This allows not only absolute size, but also a target size that is
491  * proportional to the content size.
492  *
493  * [clip] defines whether the content outside of the animated bounds should be clipped. By default,
494  * clip is set to true, which only shows content in the animated bounds.
495  *
496  * For shrinking only horizontally or vertically, consider [shrinkHorizontally], [shrinkVertically].
497  *
498  * @sample androidx.compose.animation.samples.ExpandInShrinkOutSample
499  * @param animationSpec the animation used for the shrinking animation, [spring] by default.
500  * @param shrinkTowards the ending point of the shrinking bounds, [Alignment.BottomEnd] by default.
501  * @param clip whether the content outside of the animated bounds should be clipped, true by default
502  * @param targetSize returns the end size of the shrinking bounds, `IntSize(0, 0)` by default.
503  */
504 @Stable
shrinkOutnull505 public fun shrinkOut(
506     animationSpec: FiniteAnimationSpec<IntSize> =
507         spring(
508             stiffness = Spring.StiffnessMediumLow,
509             visibilityThreshold = IntSize.VisibilityThreshold
510         ),
511     shrinkTowards: Alignment = Alignment.BottomEnd,
512     clip: Boolean = true,
513     targetSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
514 ): ExitTransition {
515     return ExitTransitionImpl(
516         TransitionData(changeSize = ChangeSize(shrinkTowards, targetSize, animationSpec, clip))
517     )
518 }
519 
520 /**
521  * This expands the clip bounds of the appearing content horizontally, from the width returned from
522  * [initialWidth] to the full width. [expandFrom] controls which part of the content gets revealed
523  * first. By default, the clip bounds animates from 0 to full width, starting from the end of the
524  * content, and expand to fully revealing the whole content.
525  *
526  * __Note__: [expandHorizontally] animates the bounds of the content. This bounds change will also
527  * result in the animation of other layouts that are dependent on this size.
528  *
529  * [initialWidth] is a lambda that takes the full width of the content and returns an initial width
530  * of the bounds of the content. This allows not only an absolute width, but also an initial width
531  * that is proportional to the content width.
532  *
533  * [clip] defines whether the content outside of the animated bounds should be clipped. By default,
534  * clip is set to true, which only shows content in the animated bounds.
535  *
536  * @sample androidx.compose.animation.samples.HorizontalTransitionSample
537  * @param animationSpec the animation used for the expanding animation, [spring] by default.
538  * @param expandFrom the starting point of the expanding bounds, [Alignment.End] by default.
539  * @param clip whether the content outside of the animated bounds should be clipped, true by default
540  * @param initialWidth the start width of the expanding bounds, returning 0 by default.
541  */
542 @Stable
expandHorizontallynull543 public fun expandHorizontally(
544     animationSpec: FiniteAnimationSpec<IntSize> =
545         spring(
546             stiffness = Spring.StiffnessMediumLow,
547             visibilityThreshold = IntSize.VisibilityThreshold
548         ),
549     expandFrom: Alignment.Horizontal = Alignment.End,
550     clip: Boolean = true,
551     initialWidth: (fullWidth: Int) -> Int = { 0 },
552 ): EnterTransition {
<lambda>null553     return expandIn(animationSpec, expandFrom.toAlignment(), clip = clip) {
554         IntSize(initialWidth(it.width), it.height)
555     }
556 }
557 
558 /**
559  * This expands the clip bounds of the appearing content vertically, from the height returned from
560  * [initialHeight] to the full height. [expandFrom] controls which part of the content gets revealed
561  * first. By default, the clip bounds animates from 0 to full height, revealing the bottom edge
562  * first, followed by the rest of the content.
563  *
564  * __Note__: [expandVertically] animates the bounds of the content. This bounds change will also
565  * result in the animation of other layouts that are dependent on this size.
566  *
567  * [initialHeight] is a lambda that takes the full height of the content and returns an initial
568  * height of the bounds of the content. This allows not only an absolute height, but also an initial
569  * height that is proportional to the content height.
570  *
571  * [clip] defines whether the content outside of the animated bounds should be clipped. By default,
572  * clip is set to true, which only shows content in the animated bounds.
573  *
574  * @sample androidx.compose.animation.samples.ExpandShrinkVerticallySample
575  * @param animationSpec the animation used for the expanding animation, [spring] by default.
576  * @param expandFrom the starting point of the expanding bounds, [Alignment.Bottom] by default.
577  * @param clip whether the content outside of the animated bounds should be clipped, true by default
578  * @param initialHeight the start height of the expanding bounds, returning 0 by default.
579  */
580 @Stable
expandVerticallynull581 public fun expandVertically(
582     animationSpec: FiniteAnimationSpec<IntSize> =
583         spring(
584             stiffness = Spring.StiffnessMediumLow,
585             visibilityThreshold = IntSize.VisibilityThreshold
586         ),
587     expandFrom: Alignment.Vertical = Alignment.Bottom,
588     clip: Boolean = true,
589     initialHeight: (fullHeight: Int) -> Int = { 0 },
590 ): EnterTransition {
<lambda>null591     return expandIn(animationSpec, expandFrom.toAlignment(), clip) {
592         IntSize(it.width, initialHeight(it.height))
593     }
594 }
595 
596 /**
597  * This shrinks the clip bounds of the disappearing content horizontally, from the full width to the
598  * width returned from [targetWidth]. [shrinkTowards] controls the direction of the bounds shrink
599  * animation. By default, the clip bounds animates from full width to 0, shrinking towards the end
600  * of the content.
601  *
602  * __Note__: [shrinkHorizontally] animates the bounds of the content. This bounds change will also
603  * result in the animation of other layouts that are dependent on this size.
604  *
605  * [targetWidth] is a lambda that takes the full width of the content and returns a target width of
606  * the content. This allows not only absolute width, but also a target width that is proportional to
607  * the content width.
608  *
609  * [clip] defines whether the content outside of the animated bounds should be clipped. By default,
610  * clip is set to true, which only shows content in the animated bounds.
611  *
612  * @sample androidx.compose.animation.samples.HorizontalTransitionSample
613  * @param animationSpec the animation used for the shrinking animation, [spring] by default.
614  * @param shrinkTowards the ending point of the shrinking bounds, [Alignment.End] by default.
615  * @param clip whether the content outside of the animated bounds should be clipped, true by default
616  * @param targetWidth returns the end width of the shrinking bounds, 0 by default.
617  */
618 @Stable
shrinkHorizontallynull619 public fun shrinkHorizontally(
620     animationSpec: FiniteAnimationSpec<IntSize> =
621         spring(
622             stiffness = Spring.StiffnessMediumLow,
623             visibilityThreshold = IntSize.VisibilityThreshold
624         ),
625     shrinkTowards: Alignment.Horizontal = Alignment.End,
626     clip: Boolean = true,
627     targetWidth: (fullWidth: Int) -> Int = { 0 }
628 ): ExitTransition {
629     // TODO: Support different animation types
<lambda>null630     return shrinkOut(animationSpec, shrinkTowards.toAlignment(), clip) {
631         IntSize(targetWidth(it.width), it.height)
632     }
633 }
634 
635 /**
636  * This shrinks the clip bounds of the disappearing content vertically, from the full height to the
637  * height returned from [targetHeight]. [shrinkTowards] controls the direction of the bounds shrink
638  * animation. By default, the clip bounds animates from full height to 0, shrinking towards the
639  * bottom of the content.
640  *
641  * __Note__: [shrinkVertically] animates the bounds of the content. This bounds change will also
642  * result in the animation of other layouts that are dependent on this size.
643  *
644  * [targetHeight] is a lambda that takes the full height of the content and returns a target height
645  * of the content. This allows not only absolute height, but also a target height that is
646  * proportional to the content height.
647  *
648  * [clip] defines whether the content outside of the animated bounds should be clipped. By default,
649  * clip is set to true, which only shows content in the animated bounds.
650  *
651  * @sample androidx.compose.animation.samples.ExpandShrinkVerticallySample
652  * @param animationSpec the animation used for the shrinking animation, [spring] by default.
653  * @param shrinkTowards the ending point of the shrinking bounds, [Alignment.Bottom] by default.
654  * @param clip whether the content outside of the animated bounds should be clipped, true by default
655  * @param targetHeight returns the end height of the shrinking bounds, 0 by default.
656  */
657 @Stable
shrinkVerticallynull658 public fun shrinkVertically(
659     animationSpec: FiniteAnimationSpec<IntSize> =
660         spring(
661             stiffness = Spring.StiffnessMediumLow,
662             visibilityThreshold = IntSize.VisibilityThreshold
663         ),
664     shrinkTowards: Alignment.Vertical = Alignment.Bottom,
665     clip: Boolean = true,
666     targetHeight: (fullHeight: Int) -> Int = { 0 },
667 ): ExitTransition {
668     // TODO: Support different animation types
<lambda>null669     return shrinkOut(animationSpec, shrinkTowards.toAlignment(), clip) {
670         IntSize(it.width, targetHeight(it.height))
671     }
672 }
673 
674 /**
675  * This slides in the content horizontally, from a starting offset defined in [initialOffsetX] to
676  * `0` **pixels**. The direction of the slide can be controlled by configuring the [initialOffsetX].
677  * A positive value means sliding from right to left, whereas a negative value would slide the
678  * content from left to right.
679  *
680  * [initialOffsetX] is a lambda that takes the full width of the content and returns an offset. This
681  * allows the starting offset to be defined proportional to the full size, or as an absolute value.
682  * It defaults to return half of negative width, which would offset the content to the left by half
683  * of its width, and slide towards the right.
684  *
685  * @sample androidx.compose.animation.samples.SlideTransition
686  * @param animationSpec the animation used for the slide-in, [spring] by default.
687  * @param initialOffsetX a lambda that takes the full width of the content in pixels and returns the
688  *   initial offset for the slide-in, by default it returns `-fullWidth/2`
689  */
690 @Stable
slideInHorizontallynull691 public fun slideInHorizontally(
692     animationSpec: FiniteAnimationSpec<IntOffset> =
693         spring(
694             stiffness = Spring.StiffnessMediumLow,
695             visibilityThreshold = IntOffset.VisibilityThreshold
696         ),
697     initialOffsetX: (fullWidth: Int) -> Int = { -it / 2 },
698 ): EnterTransition =
699     slideIn(
<lambda>null700         initialOffset = { IntOffset(initialOffsetX(it.width), 0) },
701         animationSpec = animationSpec
702     )
703 
704 /**
705  * This slides in the content vertically, from a starting offset defined in [initialOffsetY] to `0`
706  * in **pixels**. The direction of the slide can be controlled by configuring the [initialOffsetY].
707  * A positive initial offset means sliding up, whereas a negative value would slide the content
708  * down.
709  *
710  * [initialOffsetY] is a lambda that takes the full Height of the content and returns an offset.
711  * This allows the starting offset to be defined proportional to the full height, or as an absolute
712  * value. It defaults to return half of negative height, which would offset the content up by half
713  * of its Height, and slide down.
714  *
715  * @sample androidx.compose.animation.samples.FullyLoadedTransition
716  * @param animationSpec the animation used for the slide-in, [spring] by default.
717  * @param initialOffsetY a lambda that takes the full Height of the content and returns the initial
718  *   offset for the slide-in, by default it returns `-fullHeight/2`
719  */
720 @Stable
slideInVerticallynull721 public fun slideInVertically(
722     animationSpec: FiniteAnimationSpec<IntOffset> =
723         spring(
724             stiffness = Spring.StiffnessMediumLow,
725             visibilityThreshold = IntOffset.VisibilityThreshold
726         ),
727     initialOffsetY: (fullHeight: Int) -> Int = { -it / 2 },
728 ): EnterTransition =
729     slideIn(
<lambda>null730         initialOffset = { IntOffset(0, initialOffsetY(it.height)) },
731         animationSpec = animationSpec
732     )
733 
734 /**
735  * This slides out the content horizontally, from 0 to a target offset defined in [targetOffsetX] in
736  * **pixels**. The direction of the slide can be controlled by configuring the [targetOffsetX]. A
737  * positive value means sliding to the right, whereas a negative value would slide the content
738  * towards the left.
739  *
740  * [targetOffsetX] is a lambda that takes the full width of the content and returns an offset. This
741  * allows the target offset to be defined proportional to the full size, or as an absolute value. It
742  * defaults to return half of negative width, which would slide the content to the left by half of
743  * its width.
744  *
745  * @sample androidx.compose.animation.samples.SlideTransition
746  * @param animationSpec the animation used for the slide-out, [spring] by default.
747  * @param targetOffsetX a lambda that takes the full width of the content and returns the initial
748  *   offset for the slide-in, by default it returns `fullWidth/2`
749  */
750 @Stable
slideOutHorizontallynull751 public fun slideOutHorizontally(
752     animationSpec: FiniteAnimationSpec<IntOffset> =
753         spring(
754             stiffness = Spring.StiffnessMediumLow,
755             visibilityThreshold = IntOffset.VisibilityThreshold
756         ),
757     targetOffsetX: (fullWidth: Int) -> Int = { -it / 2 },
758 ): ExitTransition =
759     slideOut(
<lambda>null760         targetOffset = { IntOffset(targetOffsetX(it.width), 0) },
761         animationSpec = animationSpec
762     )
763 
764 /**
765  * This slides out the content vertically, from 0 to a target offset defined in [targetOffsetY] in
766  * **pixels**. The direction of the slide-out can be controlled by configuring the [targetOffsetY].
767  * A positive target offset means sliding down, whereas a negative value would slide the content up.
768  *
769  * [targetOffsetY] is a lambda that takes the full Height of the content and returns an offset. This
770  * allows the target offset to be defined proportional to the full height, or as an absolute value.
771  * It defaults to return half of the negative height, which would slide the content up by half of
772  * its Height.
773  *
774  * @param animationSpec the animation used for the slide-out, [spring] by default.
775  * @param targetOffsetY a lambda that takes the full Height of the content and returns the target
776  *   offset for the slide-out, by default it returns `fullHeight/2`
777  */
778 @Stable
slideOutVerticallynull779 public fun slideOutVertically(
780     animationSpec: FiniteAnimationSpec<IntOffset> =
781         spring(
782             stiffness = Spring.StiffnessMediumLow,
783             visibilityThreshold = IntOffset.VisibilityThreshold
784         ),
785     targetOffsetY: (fullHeight: Int) -> Int = { -it / 2 },
786 ): ExitTransition =
787     slideOut(
<lambda>null788         targetOffset = { IntOffset(0, targetOffsetY(it.height)) },
789         animationSpec = animationSpec
790     )
791 
792 /** ********************* Below are internal classes and methods ***************** */
793 @Immutable
794 internal data class Fade(val alpha: Float, val animationSpec: FiniteAnimationSpec<Float>)
795 
796 @Immutable
797 internal data class Slide(
798     val slideOffset: (fullSize: IntSize) -> IntOffset,
799     val animationSpec: FiniteAnimationSpec<IntOffset>
800 )
801 
802 @Immutable
803 internal data class ChangeSize(
804     val alignment: Alignment,
<lambda>null805     val size: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
806     val animationSpec: FiniteAnimationSpec<IntSize>,
807     val clip: Boolean = true
808 )
809 
810 @Immutable
811 internal data class Scale(
812     val scale: Float,
813     val transformOrigin: TransformOrigin,
814     val animationSpec: FiniteAnimationSpec<Float>
815 )
816 
817 @Immutable private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()
818 
819 @Immutable private class ExitTransitionImpl(override val data: TransitionData) : ExitTransition()
820 
Alignmentnull821 private fun Alignment.Horizontal.toAlignment() =
822     when (this) {
823         Alignment.Start -> Alignment.CenterStart
824         Alignment.End -> Alignment.CenterEnd
825         else -> Alignment.Center
826     }
827 
Alignmentnull828 private fun Alignment.Vertical.toAlignment() =
829     when (this) {
830         Alignment.Top -> Alignment.TopCenter
831         Alignment.Bottom -> Alignment.BottomCenter
832         else -> Alignment.Center
833     }
834 
835 @Immutable
836 internal data class TransitionData(
837     val fade: Fade? = null,
838     val slide: Slide? = null,
839     val changeSize: ChangeSize? = null,
840     val scale: Scale? = null,
841     val hold: Boolean = false,
842     val effectsMap: Map<TransitionEffectKey<*>, TransitionEffect> = emptyMap()
843 )
844 
845 @Suppress("UNCHECKED_CAST")
getnull846 internal operator fun <T : TransitionEffect> EnterTransition.get(key: TransitionEffectKey<T>): T? =
847     data.effectsMap[key] as? T
848 
849 @Suppress("UNCHECKED_CAST")
850 internal operator fun <T : TransitionEffect> ExitTransition.get(key: TransitionEffectKey<T>): T? =
851     data.effectsMap[key] as? T
852 
853 @OptIn(ExperimentalAnimationApi::class)
854 @Suppress("ModifierFactoryExtensionFunction", "ComposableModifierFactory")
855 @Composable
856 internal fun Transition<EnterExitState>.createModifier(
857     enter: EnterTransition,
858     exit: ExitTransition,
859     isEnabled: () -> Boolean = { true },
860     label: String
861 ): Modifier {
862     val activeEnter = trackActiveEnter(enter = enter)
863     val activeExit = trackActiveExit(exit = exit)
864 
865     val shouldAnimateSlide = activeEnter.data.slide != null || activeExit.data.slide != null
866     val shouldAnimateSizeChange =
867         activeEnter.data.changeSize != null || activeExit.data.changeSize != null
868 
869     val slideAnimation =
870         if (shouldAnimateSlide) {
<lambda>null871             createDeferredAnimation(IntOffset.VectorConverter, remember { "$label slide" })
872         } else {
873             null
874         }
875     val sizeAnimation =
876         if (shouldAnimateSizeChange) {
<lambda>null877             createDeferredAnimation(IntSize.VectorConverter, remember { "$label shrink/expand" })
878         } else null
879 
880     val offsetAnimation =
881         if (shouldAnimateSizeChange) {
882             createDeferredAnimation(
883                 IntOffset.VectorConverter,
<lambda>null884                 remember { "$label InterruptionHandlingOffset" }
885             )
886         } else null
887 
888     val disableClip =
889         (activeEnter.data.changeSize?.clip == false || activeExit.data.changeSize?.clip == false) ||
890             !shouldAnimateSizeChange
891 
892     val graphicsLayerBlock = createGraphicsLayerBlock(activeEnter, activeExit, label)
<lambda>null893     return Modifier.graphicsLayer { clip = !disableClip && isEnabled() }
894         .then(
895             EnterExitTransitionElement(
896                 this,
897                 sizeAnimation,
898                 offsetAnimation,
899                 slideAnimation,
900                 activeEnter,
901                 activeExit,
902                 isEnabled,
903                 graphicsLayerBlock
904             )
905         )
906 }
907 
908 @Composable
trackActiveEnternull909 internal fun Transition<EnterExitState>.trackActiveEnter(enter: EnterTransition): EnterTransition {
910     // Active enter & active exit reference the enter and exit transition that is currently being
911     // used. It is important to preserve the active enter/exit that was previously used before
912     // changing target state, such that if the previous enter/exit is interrupted, we still hold
913     // reference to the enter/exit that define those animations and therefore could recover.
914     var activeEnter by remember(this) { mutableStateOf(enter) }
915     if (currentState == targetState && currentState == EnterExitState.Visible) {
916         if (isSeeking) {
917             // When seeking, the timing is different and there's no need to handle interruptions.
918             activeEnter = enter
919         } else {
920             activeEnter = EnterTransition.None
921         }
922     } else if (targetState == EnterExitState.Visible) {
923         activeEnter += enter
924     }
925     return activeEnter
926 }
927 
928 @Composable
trackActiveExitnull929 internal fun Transition<EnterExitState>.trackActiveExit(exit: ExitTransition): ExitTransition {
930     // Active enter & active exit reference the enter and exit transition that is currently being
931     // used. It is important to preserve the active enter/exit that was previously used before
932     // changing target state, such that if the previous enter/exit is interrupted, we still hold
933     // reference to the enter/exit that define those animations and therefore could recover.
934     var activeExit by remember(this) { mutableStateOf(exit) }
935     if (currentState == targetState && currentState == EnterExitState.Visible) {
936         if (isSeeking) {
937             // When seeking, the timing is different and there's no need to handle interruptions.
938             activeExit = exit
939         } else {
940             activeExit = ExitTransition.None
941         }
942     } else if (targetState != EnterExitState.Visible) {
943         activeExit += exit
944     }
945     return activeExit
946 }
947 
948 internal fun interface GraphicsLayerBlockForEnterExit {
initnull949     fun init(): GraphicsLayerScope.() -> Unit
950 }
951 
952 @Composable
953 private fun Transition<EnterExitState>.createGraphicsLayerBlock(
954     enter: EnterTransition,
955     exit: ExitTransition,
956     label: String
957 ): GraphicsLayerBlockForEnterExit {
958 
959     val shouldAnimateAlpha = enter.data.fade != null || exit.data.fade != null
960     val shouldAnimateScale = enter.data.scale != null || exit.data.scale != null
961 
962     // Fade - it's important to put fade in the end. Otherwise fade will clip slide.
963     // We'll animate if at any point during the transition fadeIn/fadeOut becomes non-null. This
964     // would ensure the removal of fadeIn/Out amid a fade animation doesn't result in a jump.
965     val alphaAnimation =
966         if (shouldAnimateAlpha) {
967             createDeferredAnimation(
968                 typeConverter = Float.VectorConverter,
969                 label = remember { "$label alpha" }
970             )
971         } else null
972 
973     val scaleAnimation =
974         if (shouldAnimateScale) {
975             createDeferredAnimation(
976                 typeConverter = Float.VectorConverter,
977                 label = remember { "$label scale" }
978             )
979         } else null
980 
981     val transformOriginAnimation =
982         if (shouldAnimateScale) {
983             createDeferredAnimation(
984                 TransformOriginVectorConverter,
985                 label = "TransformOriginInterruptionHandling"
986             )
987         } else null
988 
989     return GraphicsLayerBlockForEnterExit {
990         val alpha =
991             alphaAnimation?.animate(
992                 transitionSpec = {
993                     when {
994                         EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible ->
995                             enter.data.fade?.animationSpec ?: DefaultAlphaAndScaleSpring
996                         EnterExitState.Visible isTransitioningTo EnterExitState.PostExit ->
997                             exit.data.fade?.animationSpec ?: DefaultAlphaAndScaleSpring
998                         else -> DefaultAlphaAndScaleSpring
999                     }
1000                 },
1001             ) {
1002                 when (it) {
1003                     EnterExitState.Visible -> 1f
1004                     EnterExitState.PreEnter -> enter.data.fade?.alpha ?: 1f
1005                     EnterExitState.PostExit -> exit.data.fade?.alpha ?: 1f
1006                 }
1007             }
1008 
1009         val scale =
1010             scaleAnimation?.animate(
1011                 transitionSpec = {
1012                     when {
1013                         EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible ->
1014                             enter.data.scale?.animationSpec ?: DefaultAlphaAndScaleSpring
1015                         EnterExitState.Visible isTransitioningTo EnterExitState.PostExit ->
1016                             exit.data.scale?.animationSpec ?: DefaultAlphaAndScaleSpring
1017                         else -> DefaultAlphaAndScaleSpring
1018                     }
1019                 }
1020             ) {
1021                 when (it) {
1022                     EnterExitState.Visible -> 1f
1023                     EnterExitState.PreEnter -> enter.data.scale?.scale ?: 1f
1024                     EnterExitState.PostExit -> exit.data.scale?.scale ?: 1f
1025                 }
1026             }
1027         val transformOriginWhenVisible =
1028             if (currentState == EnterExitState.PreEnter) {
1029                 enter.data.scale?.transformOrigin ?: exit.data.scale?.transformOrigin
1030             } else {
1031                 exit.data.scale?.transformOrigin ?: enter.data.scale?.transformOrigin
1032             }
1033         // Animate transform origin if there's any change. If scale is only defined for enter or
1034         // exit, use the same transform origin for both.
1035         val transformOrigin =
1036             transformOriginAnimation?.animate({ spring() }) {
1037                 when (it) {
1038                     EnterExitState.Visible -> transformOriginWhenVisible
1039                     EnterExitState.PreEnter ->
1040                         enter.data.scale?.transformOrigin ?: exit.data.scale?.transformOrigin
1041                     EnterExitState.PostExit ->
1042                         exit.data.scale?.transformOrigin ?: enter.data.scale?.transformOrigin
1043                 } ?: TransformOrigin.Center
1044             }
1045 
1046         val block: GraphicsLayerScope.() -> Unit = {
1047             this.alpha = alpha?.value ?: 1f
1048             this.scaleX = scale?.value ?: 1f
1049             this.scaleY = scale?.value ?: 1f
1050             this.transformOrigin = transformOrigin?.value ?: TransformOrigin.Center
1051         }
1052         block
1053     }
1054 }
1055 
1056 private val TransformOriginVectorConverter =
1057     TwoWayConverter<TransformOrigin, AnimationVector2D>(
<lambda>null1058         convertToVector = { AnimationVector2D(it.pivotFractionX, it.pivotFractionY) },
<lambda>null1059         convertFromVector = { TransformOrigin(it.v1, it.v2) }
1060     )
1061 
1062 private val DefaultAlphaAndScaleSpring = spring<Float>(stiffness = Spring.StiffnessMediumLow)
1063 
1064 private val DefaultOffsetAnimationSpec =
1065     spring(
1066         stiffness = Spring.StiffnessMediumLow,
1067         visibilityThreshold = IntOffset.VisibilityThreshold
1068     )
1069 
1070 private class EnterExitTransitionModifierNode(
1071     var transition: Transition<EnterExitState>,
1072     var sizeAnimation: Transition<EnterExitState>.DeferredAnimation<IntSize, AnimationVector2D>?,
1073     var offsetAnimation:
1074         Transition<EnterExitState>.DeferredAnimation<IntOffset, AnimationVector2D>?,
1075     var slideAnimation: Transition<EnterExitState>.DeferredAnimation<IntOffset, AnimationVector2D>?,
1076     var enter: EnterTransition,
1077     var exit: ExitTransition,
1078     var isEnabled: () -> Boolean,
1079     var graphicsLayerBlock: GraphicsLayerBlockForEnterExit
1080 ) : LayoutModifierNodeWithPassThroughIntrinsics() {
1081 
1082     private var lookaheadConstraintsAvailable = false
1083     private var lookaheadSize: IntSize = InvalidSize
1084     private var lookaheadConstraints: Constraints = Constraints()
1085         set(value) {
1086             lookaheadConstraintsAvailable = true
1087             field = value
1088         }
1089 
1090     var currentAlignment: Alignment? = null
1091     val alignment: Alignment?
1092         get() =
<lambda>null1093             with(transition.segment) {
1094                 if (EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible) {
1095                     enter.data.changeSize?.alignment ?: exit.data.changeSize?.alignment
1096                 } else {
1097                     exit.data.changeSize?.alignment ?: enter.data.changeSize?.alignment
1098                 }
1099             }
1100 
1101     val sizeTransitionSpec: Transition.Segment<EnterExitState>.() -> FiniteAnimationSpec<IntSize> =
<lambda>null1102         {
1103             when {
1104                 EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible ->
1105                     enter.data.changeSize?.animationSpec
1106                 EnterExitState.Visible isTransitioningTo EnterExitState.PostExit ->
1107                     exit.data.changeSize?.animationSpec
1108                 else -> DefaultSizeAnimationSpec
1109             } ?: DefaultSizeAnimationSpec
1110         }
1111 
sizeByStatenull1112     fun sizeByState(targetState: EnterExitState, fullSize: IntSize): IntSize =
1113         when (targetState) {
1114             EnterExitState.Visible -> fullSize
1115             EnterExitState.PreEnter -> enter.data.changeSize?.size?.invoke(fullSize) ?: fullSize
1116             EnterExitState.PostExit -> exit.data.changeSize?.size?.invoke(fullSize) ?: fullSize
1117         }
1118 
onAttachnull1119     override fun onAttach() {
1120         super.onAttach()
1121         lookaheadConstraintsAvailable = false
1122         lookaheadSize = InvalidSize
1123     }
1124 
1125     // This offset is only needed when the alignment value changes during the shrink/expand
1126     // animation. For example, if user specify an enter that expands from the left, and an exit
1127     // that shrinks towards the right, the asymmetric enter/exit will be brittle to interruption.
1128     // Hence the following offset animation to smooth over such interruption.
targetOffsetByStatenull1129     fun targetOffsetByState(targetState: EnterExitState, fullSize: IntSize): IntOffset =
1130         when {
1131             currentAlignment == null -> IntOffset.Zero
1132             alignment == null -> IntOffset.Zero
1133             currentAlignment == alignment -> IntOffset.Zero
1134             else ->
1135                 when (targetState) {
1136                     EnterExitState.Visible -> IntOffset.Zero
1137                     EnterExitState.PreEnter -> IntOffset.Zero
1138                     EnterExitState.PostExit ->
1139                         exit.data.changeSize?.let {
1140                             val endSize = it.size(fullSize)
1141                             val targetOffset =
1142                                 alignment!!.align(fullSize, endSize, LayoutDirection.Ltr)
1143                             val currentOffset =
1144                                 currentAlignment!!.align(fullSize, endSize, LayoutDirection.Ltr)
1145                             targetOffset - currentOffset
1146                         } ?: IntOffset.Zero
1147                 }
1148         }
1149 
measurenull1150     override fun MeasureScope.measure(
1151         measurable: Measurable,
1152         constraints: Constraints
1153     ): MeasureResult {
1154         if (transition.currentState == transition.targetState) {
1155             currentAlignment = null
1156         } else if (currentAlignment == null) {
1157             currentAlignment = alignment ?: Alignment.TopStart
1158         }
1159         if (isLookingAhead) {
1160             val placeable = measurable.measure(constraints)
1161             val measuredSize = IntSize(placeable.width, placeable.height)
1162             lookaheadSize = measuredSize
1163             lookaheadConstraints = constraints
1164             return layout(measuredSize.width, measuredSize.height) { placeable.place(0, 0) }
1165         } else if (isEnabled()) {
1166             val layerBlock = graphicsLayerBlock.init()
1167             // Measure the content based on the current constraints passed down from parent.
1168             // AnimatedContent will measure outgoing children with a cached constraints to avoid
1169             // re-layout the outgoing content. At the animateEnterExit() level, it's not best not
1170             // to make assumptions, which is why we use constraints from parent.
1171             val placeable = measurable.measure(constraints)
1172             val measuredSize = IntSize(placeable.width, placeable.height)
1173             val target = if (lookaheadSize.isValid) lookaheadSize else measuredSize
1174             val animSize = sizeAnimation?.animate(sizeTransitionSpec) { sizeByState(it, target) }
1175             // Since we measure with lookahead constraints when available, the size needs to
1176             // be constrained by incoming constraints so that we know how to position content
1177             // in the constrained rect based on alignment.
1178             val currentSize = constraints.constrain(animSize?.value ?: measuredSize)
1179             val offsetDelta =
1180                 offsetAnimation
1181                     ?.animate({ DefaultOffsetAnimationSpec }) { targetOffsetByState(it, target) }
1182                     ?.value ?: IntOffset.Zero
1183             val slideOffset =
1184                 slideAnimation?.animate(slideSpec) { slideTargetValueByState(it, target) }?.value
1185                     ?: IntOffset.Zero
1186             val offset =
1187                 (currentAlignment?.align(target, currentSize, LayoutDirection.Ltr)
1188                     ?: IntOffset.Zero) + slideOffset
1189             return layout(currentSize.width, currentSize.height) {
1190                 placeable.placeWithLayer(
1191                     offset.x + offsetDelta.x,
1192                     offset.y + offsetDelta.y,
1193                     0f,
1194                     layerBlock
1195                 )
1196             }
1197         } else {
1198             // If not enabled, skip all animations
1199             return measurable.measure(constraints).run { layout(width, height) { place(0, 0) } }
1200         }
1201     }
1202 
<lambda>null1203     val slideSpec: Transition.Segment<EnterExitState>.() -> FiniteAnimationSpec<IntOffset> = {
1204         when {
1205             EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible -> {
1206                 enter.data.slide?.animationSpec ?: DefaultOffsetAnimationSpec
1207             }
1208             EnterExitState.Visible isTransitioningTo EnterExitState.PostExit -> {
1209                 exit.data.slide?.animationSpec ?: DefaultOffsetAnimationSpec
1210             }
1211             else -> DefaultOffsetAnimationSpec
1212         }
1213     }
1214 
slideTargetValueByStatenull1215     fun slideTargetValueByState(targetState: EnterExitState, fullSize: IntSize): IntOffset {
1216         val preEnter = enter.data.slide?.slideOffset?.invoke(fullSize) ?: IntOffset.Zero
1217         val postExit = exit.data.slide?.slideOffset?.invoke(fullSize) ?: IntOffset.Zero
1218         return when (targetState) {
1219             EnterExitState.Visible -> IntOffset.Zero
1220             EnterExitState.PreEnter -> preEnter
1221             EnterExitState.PostExit -> postExit
1222         }
1223     }
1224 }
1225 
1226 private val DefaultSizeAnimationSpec =
1227     spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = IntSize.VisibilityThreshold)
1228 
1229 private data class EnterExitTransitionElement(
1230     val transition: Transition<EnterExitState>,
1231     var sizeAnimation: Transition<EnterExitState>.DeferredAnimation<IntSize, AnimationVector2D>?,
1232     var offsetAnimation:
1233         Transition<EnterExitState>.DeferredAnimation<IntOffset, AnimationVector2D>?,
1234     var slideAnimation: Transition<EnterExitState>.DeferredAnimation<IntOffset, AnimationVector2D>?,
1235     var enter: EnterTransition,
1236     var exit: ExitTransition,
1237     var isEnabled: () -> Boolean,
1238     var graphicsLayerBlock: GraphicsLayerBlockForEnterExit
1239 ) : ModifierNodeElement<EnterExitTransitionModifierNode>() {
createnull1240     override fun create(): EnterExitTransitionModifierNode =
1241         EnterExitTransitionModifierNode(
1242             transition,
1243             sizeAnimation,
1244             offsetAnimation,
1245             slideAnimation,
1246             enter,
1247             exit,
1248             isEnabled,
1249             graphicsLayerBlock
1250         )
1251 
1252     override fun update(node: EnterExitTransitionModifierNode) {
1253         node.transition = transition
1254         node.sizeAnimation = sizeAnimation
1255         node.offsetAnimation = offsetAnimation
1256         node.slideAnimation = slideAnimation
1257         node.enter = enter
1258         node.exit = exit
1259         node.isEnabled = isEnabled
1260         node.graphicsLayerBlock = graphicsLayerBlock
1261     }
1262 
inspectablePropertiesnull1263     override fun InspectorInfo.inspectableProperties() {
1264         name = "enterExitTransition"
1265         properties["transition"] = transition
1266         properties["sizeAnimation"] = sizeAnimation
1267         properties["offsetAnimation"] = offsetAnimation
1268         properties["slideAnimation"] = slideAnimation
1269         properties["enter"] = enter
1270         properties["exit"] = exit
1271         properties["graphicsLayerBlock"] = graphicsLayerBlock
1272     }
1273 }
1274