AnimatedContent
@Composable
public fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
(fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
.togetherWith(fadeOut(animationSpec = tween(90)))
},
contentAlignment: Alignment = Alignment.TopStart,
label: String = "AnimatedContent",
contentKey: (targetState: S) -> Any? = { it },
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit,
)
AnimatedContent
is a container that automatically animates its content when targetState
changes. Its content
for different target states is defined in a mapping between a target state
and a composable function.
IMPORTANT: The targetState parameter for the content
lambda should always be taken into
account in deciding what composable function to return as the content for that state. This is
critical to ensure a successful lookup of all the incoming and outgoing content during content
transform.
When targetState
changes, content for both new and previous targetState will be looked up
through the content
lambda. They will go through a ContentTransform
so that the new target
content can be animated in while the initial content animates out. Meanwhile the container will
animate its size as needed to accommodate the new content, unless SizeTransform
is set to
null
. Once the ContentTransform
is finished, the outgoing content will be disposed.
If targetState
is expected to mutate frequently and not all mutations should be treated as
target state change, consider defining a mapping between targetState
and a key in contentKey
.
As a result, transitions will be triggered when the resulting key changes. In other words, there
will be no animation when switching between targetState
s that share the same key. By default,
the key will be the same as the targetState object.
By default, the ContentTransform
will be a delayed fadeIn
of the target content and a delayed
scaleIn
togetherWith
a fadeOut
of the initial content, using a SizeTransform
to animate
any size change of the content. This behavior can be customized using transitionSpec
. If
desired, different ContentTransform
s can be defined for different pairs of initial content and
target content.
AnimatedContent
displays only the content for targetState
when not animating. However, during
the transient content transform, there will be more than one set of content present in the
AnimatedContent
container. It may be sometimes desired to define the positional relationship
among the different content and the overlap. This can be achieved by defining contentAlignment
and zOrder
. By default, contentAlignment
aligns all
content to Alignment.TopStart
, and the zIndex
for all the content is 0f. Note: The target
content will always be placed last, therefore it will be on top of all the other content unless
zIndex is specified.
Different content in AnimatedContent
will have access to their own AnimatedContentScope
. This
allows content to define more local enter/exit transitions via
AnimatedContentScope.animateEnterExit
and AnimatedContentScope.transition
. These custom
enter/exit animations will be triggered as the content enters/leaves the container.
label
is an optional parameter to differentiate from other animations in Android Studio.
Below is an example of customizing transitionSpec
to imply a spatial relationship between the
content for different states:
@Composable
public fun <S> Transition<S>.AnimatedContent(
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
(fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
.togetherWith(fadeOut(animationSpec = tween(90)))
},
contentAlignment: Alignment = Alignment.TopStart,
contentKey: (targetState: S) -> Any? = { it },
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit,
)
AnimatedContent
is a container that automatically animates its content when
Transition.targetState
changes. Its content
for different target states is defined in a
mapping between a target state and a composable function.
IMPORTANT: The targetState parameter for the content
lambda should always be taken into
account in deciding what composable function to return as the content for that state. This is
critical to ensure a successful lookup of all the incoming and outgoing content during content
transform.
When Transition.targetState
changes, content for both new and previous targetState will be
looked up through the content
lambda. They will go through a ContentTransform
so that the new
target content can be animated in while the initial content animates out. Meanwhile the container
will animate its size as needed to accommodate the new content, unless SizeTransform
is set to
null
. Once the ContentTransform
is finished, the outgoing content will be disposed.
If Transition.targetState
is expected to mutate frequently and not all mutations should be
treated as target state change, consider defining a mapping between Transition.targetState
and
a key in contentKey
. As a result, transitions will be triggered when the resulting key changes.
In other words, there will be no animation when switching between Transition.targetState
s that
share the same key. By default, the key will be the same as the targetState object.
By default, the ContentTransform
will be a delayed fadeIn
of the target content and a delayed
scaleIn
togetherWith
a fadeOut
of the initial content, using a SizeTransform
to animate
any size change of the content. This behavior can be customized using transitionSpec
. If
desired, different ContentTransform
s can be defined for different pairs of initial content and
target content.
AnimatedContent
displays only the content for Transition.targetState
when not animating.
However, during the transient content transform, there will be more than one sets of content
present in the AnimatedContent
container. It may be sometimes desired to define the positional
relationship among different content and the style of overlap. This can be achieved by defining
contentAlignment
and zOrder
. By default,
contentAlignment
aligns all content to Alignment.TopStart
, and the zIndex
for all the
content is 0f. Note: The target content will always be placed last, therefore it will be on
top of all the other content unless zIndex is specified.
Different content in AnimatedContent
will have access to their own AnimatedContentScope
. This
allows content to define more local enter/exit transitions via
AnimatedContentScope.animateEnterExit
and AnimatedContentScope.transition
. These custom
enter/exit animations will be triggered as the content enters/leaves the container.
Code Examples
SimpleAnimatedContentSample
@Composable
fun SimpleAnimatedContentSample() {
// enum class ContentState { Foo, Bar, Baz }
@Composable
fun Foo() {
Box(Modifier.size(200.dp).background(Color(0xffffdb00)))
}
@Composable
fun Bar() {
Box(Modifier.size(40.dp).background(Color(0xffff8100)))
}
@Composable
fun Baz() {
Box(Modifier.size(80.dp, 20.dp).background(Color(0xffff4400)))
}
var contentState: ContentState by remember { mutableStateOf(ContentState.Foo) }
AnimatedContent(contentState) {
when (it) {
// Specifies the mapping between a given ContentState and a composable function.
ContentState.Foo -> Foo()
ContentState.Bar -> Bar()
ContentState.Baz -> Baz()
}
}
}
AnimateIncrementDecrementSample
@Composable
fun AnimateIncrementDecrementSample() {
Column(Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
var count by remember { mutableStateOf(0) }
// The `AnimatedContent` below uses an integer count as its target state. So when the
// count changes, it will animate out the content associated with the previous count, and
// animate in the content associated with the target state.
AnimatedContent(
targetState = count,
transitionSpec = {
// We can define how the new target content comes in and how initial content
// leaves in the ContentTransform. Here we want to create the impression that the
// different numbers have a spatial relationship - larger numbers are
// positioned (vertically) below smaller numbers.
if (targetState > initialState) {
// If the incoming number is larger, new number slides up and fades in while
// the previous (smaller) number slides up to make room and fades out.
slideInVertically { it } + fadeIn() togetherWith
slideOutVertically { -it } + fadeOut()
} else {
// If the incoming number is smaller, new number slides down and fades in
// while
// the previous number slides down and fades out.
slideInVertically { -it } + fadeIn() togetherWith
slideOutVertically { it } + fadeOut()
// Disable clipping since the faded slide-out is desired out of bounds, but
// the size transform is still needed from number getting longer
}
.using(SizeTransform(clip = false)) // Using default spring for the size change.
},
) { targetCount ->
// This establishes a mapping between the target state and the content in the form of a
// Composable function. IMPORTANT: The parameter of this content lambda should
// *always* be used. During the content transform, the old content will be looked up
// using this lambda with the old state, until it's fully animated out.
// Since AnimatedContent differentiates the contents using their target states as the
// key, the same composable function returned by the content lambda like below will be
// invoked under different keys and therefore treated as different entities.
Text("$targetCount", fontSize = 20.sp)
}
Spacer(Modifier.size(20.dp))
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = { count-- }) { Text("Minus") }
Spacer(Modifier.size(60.dp))
Button(onClick = { count++ }) { Text("Plus ") }
}
}
}
TransitionExtensionAnimatedContentSample
@Composable
fun TransitionExtensionAnimatedContentSample() {
@Composable
fun CollapsedCart() {
/* Some content here */
}
@Composable
fun ExpandedCart() {
/* Some content here */
}
// enum class CartState { Expanded, Collapsed }
var cartState by remember { mutableStateOf(CartState.Collapsed) }
// Creates a transition here to animate the corner shape and content.
val cartOpenTransition = updateTransition(cartState, "CartOpenTransition")
val cornerSize by
cartOpenTransition.animateDp(
label = "cartCornerSize",
transitionSpec = {
when {
CartState.Expanded isTransitioningTo CartState.Collapsed ->
tween(durationMillis = 433, delayMillis = 67)
else -> tween(durationMillis = 150)
}
},
) {
if (it == CartState.Expanded) 0.dp else 24.dp
}
Surface(
Modifier.shadow(8.dp, CutCornerShape(topStart = cornerSize))
.clip(CutCornerShape(topStart = cornerSize)),
color = Color(0xfffff0ea),
) {
// Creates an AnimatedContent using the transition. This AnimatedContent will
// derive its target state from cartOpenTransition.targetState. All the animations
// created inside of AnimatedContent for size change, enter/exit will be added to the
// Transition.
cartOpenTransition.AnimatedContent(
transitionSpec = {
fadeIn(animationSpec = tween(150, delayMillis = 150))
.togetherWith(fadeOut(animationSpec = tween(150)))
.using(
SizeTransform { initialSize, targetSize ->
// Using different SizeTransform for different state change
if (CartState.Collapsed isTransitioningTo CartState.Expanded) {
keyframes {
durationMillis = 500
// Animate to full target width and by 200px in height at 150ms
IntSize(targetSize.width, initialSize.height + 200) at 150
}
} else {
keyframes {
durationMillis = 500
// Animate 1/2 the height without changing the width at 150ms.
// The width and rest of the height will be animated in the
// timeframe between 150ms and duration (i.e. 500ms)
IntSize(
initialSize.width,
(initialSize.height + targetSize.height) / 2,
) at 150
}
}
}
)
.apply {
targetContentZIndex =
when (targetState) {
// This defines a relationship along z-axis during the momentary
// overlap as both incoming and outgoing content is on screen. This
// fixed zOrder will ensure that collapsed content will always be on
// top of the expanded content - it will come in on top, and
// disappear over the expanded content as well.
CartState.Expanded -> 1f
CartState.Collapsed -> 2f
}
}
}
) {
// This defines the mapping from state to composable. It's critical to use the state
// parameter (i.e. `it`) that is passed into this block of code to ensure correct
// content lookup.
when (it) {
CartState.Expanded -> ExpandedCart()
CartState.Collapsed -> CollapsedCart()
}
}
}
}