I'm trying to animate a size change inside a Dialog in Jetpack Compose using AnimatedContent. The goal is to smoothly transition between two different UI states that have different heights. However, the size animation doesn't work correctly as it can be seen in the gif below.
Here is a simplified version of my code. The Box elements are just placeholders to test different heights.
@Composable
fun DialogWithAnimation() {
var state by remember { mutableIntStateOf(0) }
Dialog(onDismissRequest = {}) {
Surface(shape = AlertDialogDefaults.shape) {
Column {
// Header
Row(
modifier = Modifier
.height(40.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
MyHeaderButton(
modifier = Modifier.weight(1f),
isActive = state == 0,
label = "A"
) { state = 0 }
MyHeaderButton(
modifier = Modifier.weight(1f),
isActive = state == 1,
label = "B"
) { state = 1 }
}
AnimatedContent(
targetState = state,
label = "A-B-content",
transitionSpec = {
(fadeIn(animationSpec = tween(220)) togetherWith
fadeOut(animationSpec = tween(90)))
}
) { target ->
if (target == 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Blue)
)
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.background(Color.Yellow)
)
}
}
}
}
}
}
I expected the content inside the dialog to animate smoothly when changing its height.
This is the current behavior:
With dynamic content
output: https://imgur.com/a/qB7akQ6
@Composable
fun DialogWithAnimation(onDismissRequest: () -> Unit) {
var tab by remember { mutableIntStateOf(0) }
Dialog(onDismissRequest = onDismissRequest) {
Surface(
modifier = Modifier.padding(16.dp),
shape = RoundedCornerShape(16.dp),
tonalElevation = 4.dp
) {
Column(
modifier = Modifier
.padding(16.dp)
.width(IntrinsicSize.Min)
) {
// Tab Buttons
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
MyHeaderButton(
modifier = Modifier.weight(1f),
isActive = tab == 0,
label = "Short"
) { tab = 0 }
MyHeaderButton(
modifier = Modifier.weight(1f),
isActive = tab == 1,
label = "Long"
) { tab = 1 }
}
Spacer(Modifier.height(12.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(
animationSpec = tween(
durationMillis = 50,
easing = LinearEasing
)
)
.clip(RoundedCornerShape(12.dp))
.background(Color.LightGray)
) {
when (tab) {
0 -> {
ShortContent()
}
else ->{
LongContent()
}
}
}
}
}
}
}
@Composable
fun ShortContent() {
Column(
Modifier
.fillMaxWidth()
.background(Color.Blue)
.padding(8.dp)
) {
Text("Short content", color = Color.White)
}
}
@Composable
fun LongContent() {
Column(
Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(8.dp)
) {
Text("This is longer content that will grow", color = Color.White)
Spacer(modifier = Modifier.height(16.dp))
Text("It should animate height linearly.", color = Color.White)
Spacer(modifier = Modifier.height(16.dp))
Text("No sudden push, just smooth expansion.", color = Color.White)
}
}
@Composable
fun MyHeaderButton(
modifier: Modifier = Modifier,
isActive: Boolean,
label: String,
onClick: () -> Unit
) {
val backgroundColor by animateColorAsState(
targetValue = if (isActive) MaterialTheme.colorScheme.primary else Color.Transparent,
animationSpec = tween(durationMillis = 300),
label = "bg-color"
)
val contentColor by animateColorAsState(
targetValue = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
animationSpec = tween(durationMillis = 300),
label = "text-color"
)
Surface(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onClick),
color = backgroundColor,
border = if (isActive) null else BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
tonalElevation = if (isActive) 2.dp else 0.dp,
shadowElevation = 0.dp,
) {
Box(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = label,
color = contentColor,
style = MaterialTheme.typography.labelLarge
)
}
}
}