Jetpack Compose: Animaciones Avanzadas Paso a Paso
Guía completa de animaciones avanzadas en Compose: transiciones, AnimatedVisibility, Animatable, gestos y rendimiento para interfaces Android fluidas.

Las animaciones convierten una aplicación funcional en una experiencia memorable. Jetpack Compose ofrece una API declarativa potente que simplifica enormemente la creación de interfaces fluidas. Esta guía explora técnicas avanzadas para construir animaciones eficientes y mantenibles.
Este tutorial asume familiaridad con los fundamentos de Compose (recomposición, estado, modifiers). Para revisar lo básico, conviene consultar primero la guía de preguntas de entrevista sobre Jetpack Compose.
Fundamentos de la API de Animaciones en Compose
Compose proporciona varios niveles de API para las animaciones. La elección depende del nivel de control deseado y de la complejidad de la animación.
La API se divide en tres categorías principales: animaciones de alto nivel (AnimatedVisibility, AnimatedContent), animaciones basadas en estado (animate*AsState) y animaciones de bajo nivel (Animatable, Transition).
// Overview of the three animation API levels
@Composable
fun AnimationApiOverview() {
// High level: simple predefined animations
AnimatedVisibility(visible = isVisible) {
Text("Animated content")
}
// Intermediate level: state-driven animation
val alpha by animateFloatAsState(
targetValue = if (isSelected) 1f else 0.5f,
label = "alpha"
)
// Low level: full control over animation
val animatable = remember { Animatable(0f) }
LaunchedEffect(targetValue) {
animatable.animateTo(targetValue)
}
}Elegir el nivel de API adecuado resulta crucial para mantener un código legible conservando la flexibilidad necesaria.
AnimatedVisibility: Animaciones Elegantes de Entrada y Salida
AnimatedVisibility es el punto de entrada ideal para animar la aparición y desaparición de elementos. Esta API gestiona automáticamente la composición y descomposición del contenido.
Los parámetros enter y exit aceptan combinaciones de transiciones que definen el comportamiento de la animación. Estas transiciones pueden combinarse mediante el operador +.
@Composable
fun ExpandableCard(
title: String,
content: String,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = modifier.clickable { isExpanded = !isExpanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Icon(
imageVector = if (isExpanded) Icons.Default.ExpandLess
else Icons.Default.ExpandMore,
contentDescription = null
)
}
// Expansion animation with fade + slide
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn(animationSpec = tween(300)) +
expandVertically(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200)) +
shrinkVertically(animationSpec = tween(200))
) {
Text(
text = content,
modifier = Modifier.padding(top = 12.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}El contenido dentro de AnimatedVisibility solo se compone cuando visible = true, lo que optimiza el rendimiento en listas con muchos elementos expandibles.
Las transiciones disponibles incluyen fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Pueden combinarse libremente para crear efectos personalizados.
animate*AsState: Animaciones Basadas en Estado
La familia de funciones animate*AsState anima automáticamente los cambios de valores primitivos. Es el enfoque más idiomático para animaciones simples en Compose.
Cada tipo de dato tiene su función dedicada: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState, etc.
@Composable
fun InteractiveButton(
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
// Animated background color
val backgroundColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 250),
label = "backgroundColor"
)
// Animated elevation
val elevation by animateDpAsState(
targetValue = if (isSelected) 8.dp else 2.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "elevation"
)
// Animated text size
val textSize by animateFloatAsState(
targetValue = if (isSelected) 18f else 14f,
label = "textSize"
)
Surface(
modifier = modifier.clickable(onClick = onClick),
color = backgroundColor,
shadowElevation = elevation,
shape = RoundedCornerShape(12.dp)
) {
Text(
text = if (isSelected) "Selected" else "Select",
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
fontSize = textSize.sp
)
}
}El parámetro animationSpec controla el comportamiento temporal de la animación. Los dos specs más utilizados son tween (duración fija con easing) y spring (física realista con rebote).
Transition: Orquestar Múltiples Animaciones
Cuando varias propiedades deben animarse de forma coordinada, updateTransition proporciona un control centralizado. Esta API garantiza que todas las animaciones permanezcan sincronizadas.
El patrón consiste en definir un estado enum y luego crear animaciones para cada propiedad que dependa de ese estado.
// Card state: defines visual behavior
enum class CardState { Collapsed, Expanded, Selected }
@Composable
fun AnimatedStateCard(
cardState: CardState,
modifier: Modifier = Modifier
) {
// Central transition coordinating all animations
val transition = updateTransition(
targetState = cardState,
label = "cardTransition"
)
// Card height based on state
val cardHeight by transition.animateDp(
transitionSpec = { spring(stiffness = Spring.StiffnessLow) },
label = "height"
) { state ->
when (state) {
CardState.Collapsed -> 80.dp
CardState.Expanded -> 200.dp
CardState.Selected -> 160.dp
}
}
// Border color based on state
val borderColor by transition.animateColor(
transitionSpec = { tween(300) },
label = "borderColor"
) { state ->
when (state) {
CardState.Collapsed -> Color.Transparent
CardState.Expanded -> MaterialTheme.colorScheme.outline
CardState.Selected -> MaterialTheme.colorScheme.primary
}
}
// Corner radius based on state
val cornerRadius by transition.animateDp(
label = "cornerRadius"
) { state ->
when (state) {
CardState.Collapsed -> 8.dp
CardState.Expanded -> 16.dp
CardState.Selected -> 24.dp
}
}
Card(
modifier = modifier
.height(cardHeight)
.border(2.dp, borderColor, RoundedCornerShape(cornerRadius)),
shape = RoundedCornerShape(cornerRadius)
) {
// Card content
}
}¿Listo para aprobar tus entrevistas de Android?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Animatable: Control Total de la Animación
Animatable es la API de bajo nivel que ofrece un control programático completo. Este enfoque resulta necesario para animaciones interrumpibles, gestos o escenarios complejos.
A diferencia de animate*AsState, Animatable permite detener, invertir o modificar una animación en curso sin esperar a que finalice.
@Composable
fun SwipeableCard(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
// Horizontal offset controlled by Animatable
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
// Swipe threshold to trigger dismissal
val dismissThreshold = 300f
Box(
modifier = modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
scope.launch {
if (abs(offsetX.value) > dismissThreshold) {
// Animate out then callback
val target = if (offsetX.value > 0) 1000f else -1000f
offsetX.animateTo(
targetValue = target,
animationSpec = tween(200)
)
onDismiss()
} else {
// Return to initial position with spring
offsetX.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy
)
)
}
}
},
onHorizontalDrag = { _, dragAmount ->
scope.launch {
// snapTo for instant finger tracking
offsetX.snapTo(offsetX.value + dragAmount)
}
}
)
}
) {
content()
}
}Los métodos clave de Animatable son animateTo() (animar hacia un objetivo), snapTo() (cambio instantáneo) y stop() (interrupción).
AnimatedContent: Transiciones de Contenido
AnimatedContent anima las transiciones entre diferentes contenidos. Esta API resulta perfecta para cambios de estado que modifican completamente la interfaz mostrada.
La clave targetState determina cuándo debe ocurrir una transición. transitionSpec define cómo interactúan el contenido saliente y el entrante.
@Composable
fun CounterWithAnimation(
count: Int,
modifier: Modifier = Modifier
) {
AnimatedContent(
targetState = count,
modifier = modifier,
transitionSpec = {
// Determine animation direction
val direction = if (targetState > initialState) {
// New number enters from top
slideInVertically { height -> -height } + fadeIn() togetherWith
slideOutVertically { height -> height } + fadeOut()
} else {
// New number enters from bottom
slideInVertically { height -> height } + fadeIn() togetherWith
slideOutVertically { height -> -height } + fadeOut()
}
direction.using(SizeTransform(clip = false))
},
label = "counter"
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold
)
}
}El contenido dentro de AnimatedContent se recompone con cada cambio de targetState. Para contenido complejo, conviene memorizar los elementos costosos o usar una estrategia de claves apropiada.
Animaciones Infinitas con rememberInfiniteTransition
Para animaciones en bucle (indicadores de carga, efectos de pulso), rememberInfiniteTransition ofrece una API dedicada que no requiere gestión manual del ciclo.
@Composable
fun PulsingDot(
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
// Looping scale animation
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
// Synchronized opacity animation
val alpha by infiniteTransition.animateFloat(
initialValue = 0.5f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = modifier
.size(24.dp)
.scale(scale)
.alpha(alpha)
.background(color = color, shape = CircleShape)
)
}Animaciones de Listas con LazyColumn
Las animaciones de elementos de lista requieren atención especial. El modifier animateItem() (antes animateItemPlacement) anima automáticamente las reordenaciones.
@Composable
fun AnimatedTaskList(
tasks: List<Task>,
onToggle: (Task) -> Unit,
onDelete: (Task) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = tasks,
key = { it.id } // Stable key required for animateItem
) { task ->
var isVisible by remember { mutableStateOf(true) }
// Exit animation before deletion
AnimatedVisibility(
visible = isVisible,
exit = shrinkVertically() + fadeOut()
) {
TaskItem(
task = task,
onToggle = { onToggle(task) },
onDelete = {
isVisible = false
// Delay to let animation complete
},
modifier = Modifier.animateItem(
fadeInSpec = tween(300),
fadeOutSpec = tween(300),
placementSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
// Trigger deletion after animation
LaunchedEffect(isVisible) {
if (!isVisible) {
delay(300)
onDelete(task)
}
}
}
}
}Sin un parámetro key estable, animateItem no puede rastrear los elementos entre recomposiciones. Conviene usar un ID único en lugar del índice de la lista.
Animaciones con Canvas
Para efectos visuales personalizados, combinar Canvas con valores animados ofrece flexibilidad total.
@Composable
fun AnimatedProgressRing(
progress: Float, // 0f to 1f
modifier: Modifier = Modifier
) {
// Progress animation with spring for natural feel
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessVeryLow
),
label = "progress"
)
// Continuous rotation animation
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
),
label = "rotation"
)
val primaryColor = MaterialTheme.colorScheme.primary
val trackColor = MaterialTheme.colorScheme.surfaceVariant
Canvas(
modifier = modifier
.size(120.dp)
.rotate(rotation)
) {
val strokeWidth = 12.dp.toPx()
val radius = (size.minDimension - strokeWidth) / 2
// Background circle (track)
drawCircle(
color = trackColor,
radius = radius,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
// Animated progress arc
drawArc(
color = primaryColor,
startAngle = -90f,
sweepAngle = animatedProgress * 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round),
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(radius * 2, radius * 2)
)
}
}¿Listo para aprobar tus entrevistas de Android?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Optimización del Rendimiento de Animaciones
Las animaciones mal optimizadas pueden provocar jank y agotar la batería. Estas son las buenas prácticas para mantener 60 FPS.
La primera regla consiste en evitar las asignaciones durante la animación. Conviene usar graphicsLayer en lugar de modifiers que disparan recomposiciones.
@Composable
fun OptimizedAnimatedCard(
isExpanded: Boolean,
modifier: Modifier = Modifier
) {
val scale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f,
label = "scale"
)
val alpha by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0.8f,
label = "alpha"
)
Card(
modifier = modifier
// ✅ graphicsLayer: GPU modifications without recomposition
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
}
// ❌ Avoid: .scale(scale).alpha(alpha)
// These modifiers trigger recompositions
) {
Text("Card content")
}
}
// Example with remembered lambda to avoid allocations
@Composable
fun OptimizedClickableItem(
onClick: () -> Unit,
content: @Composable () -> Unit
) {
// ✅ Stable remembered lambda
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick
)
) {
content()
}
}El segundo punto crítico atañe a las animaciones en listas. Conviene limitar el número de animaciones simultáneas y usar derivedStateOf para los cálculos derivados.
@Composable
fun PerformantAnimatedList(
items: List<Item>,
modifier: Modifier = Modifier
) {
// Calculate once whether the list is empty
val isEmpty by remember {
derivedStateOf { items.isEmpty() }
}
LazyColumn(modifier = modifier) {
items(
items = items,
key = { it.id }
) { item ->
// Lightweight animation only on initial appearance
var hasAppeared by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
hasAppeared = true
}
val alpha by animateFloatAsState(
targetValue = if (hasAppeared) 1f else 0f,
animationSpec = tween(200),
label = "itemAlpha"
)
ItemCard(
item = item,
modifier = Modifier.graphicsLayer { this.alpha = alpha }
)
}
}
}Conclusión
Las animaciones en Jetpack Compose ofrecen un equilibrio entre facilidad de uso y control avanzado. Estos son los puntos clave a recordar:
- ✅ Elegir el nivel de API adecuado según la complejidad (AnimatedVisibility → animate*AsState → Animatable)
- ✅ Usar
updateTransitionpara coordinar varias animaciones relacionadas - ✅ Preferir
springpara animaciones naturales ytweenpara duraciones precisas - ✅ Proporcionar siempre un parámetro
keyestable en las animaciones de listas - ✅ Optimizar con
graphicsLayerpara evitar recomposiciones innecesarias - ✅ Probar las animaciones en dispositivos reales para validar el rendimiento
Dominar las animaciones de Compose distingue a las aplicaciones Android profesionales. Estas técnicas, combinadas con atención al rendimiento, permiten crear experiencias de usuario fluidas y atractivas.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Las 20 preguntas más frecuentes de Jetpack Compose en entrevistas (2026)
Las 20 preguntas de entrevista sobre Jetpack Compose más comunes: recomposición, gestión de estado, navegación, rendimiento y patrones de arquitectura.

Kotlin 2.3 para Android: Desestructuración por Nombre, KMP y Preguntas de Entrevista 2026
Preguntas de entrevista sobre Kotlin 2.3 para desarrolladores Android en 2026. Desestructuración por nombre, KMP, parámetros de contexto, Flow y coroutines con ejemplos de código.

MVVM vs MVI en Android: ¿Qué arquitectura elegir en 2026?
Comparación profunda entre MVVM y MVI en Android: ventajas, limitaciones, casos de uso y una guía práctica para elegir la arquitectura correcta en 2026.