# 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. - Published: 2026-02-08 - Updated: 2026-04-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- 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. > **Requisitos previos** > > 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`). ```kotlin // AnimationLevels.kt // 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 `+`. ```kotlin // AnimatedVisibilityExample.kt @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. > **Combinación de transiciones** > > 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. ```kotlin // AnimateAsStateExample.kt @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. ```kotlin // TransitionExample.kt // 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 } } ``` ## 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. ```kotlin // AnimatableExample.kt @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. ```kotlin // AnimatedContentExample.kt @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 ) } } ``` > **Rendimiento con AnimatedContent** > > 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. ```kotlin // InfiniteTransitionExample.kt @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. ```kotlin // ListAnimationExample.kt @Composable fun AnimatedTaskList( tasks: List, 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) } } } } } ``` > **Claves estables para animaciones de listas** > > 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. ```kotlin // CanvasAnimationExample.kt @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) ) } } ``` ## 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. ```kotlin // PerformanceOptimization.kt @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. ```kotlin // ListPerformance.kt @Composable fun PerformantAnimatedList( items: List, 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 `updateTransition` para coordinar varias animaciones relacionadas - ✅ Preferir `spring` para animaciones naturales y `tween` para duraciones precisas - ✅ Proporcionar siempre un parámetro `key` estable en las animaciones de listas - ✅ Optimizar con `graphicsLayer` para 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/android/jetpack-compose-advanced-animations