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