[AnimatedContent] is a container that automatically animates its content when [targetState] changes.
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 ") }
}
}
}
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()
}
}
}
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()
}
}
}
}