Blog para desarrollo de aplicaciones en Android, aprende paso a paso como crear aplicaciones.

Usamos cookies propias y de terceros que entre otras cosas recogen datos sobre sus hábitos de navegación para mostrarle publicidad personalizada y realizar análisis de uso de nuestro sitio.
Si continúa navegando consideramos que acepta su uso. OK Más información | Y más

Transiciones en Jetpack Compose

Transiciones en Jetpack Compose

Hola amigos 👋 Bienvenidos a un nuevo tutorial de Universo Android. Hoy aprenderemos a crear animaciones y transiciones en Jetpack Compose, el framework moderno de UI declarativa de Android que hace las animaciones más simples y poderosas.

Al finalizar este tutorial tendrás una aplicación que incluirá:

  • Animaciones básicas con animate*AsState
  • Transiciones con AnimatedVisibility
  • Animaciones de contenido con AnimatedContent
  • Transiciones personalizadas
  • Animaciones infinitas
  • Gestos animados
  • Ejemplos prácticos

🟩 1. ¿Qué son las Transiciones en Compose?

Las transiciones en Compose son animaciones declarativas que responden a cambios de estado. Son más simples que las animaciones tradicionales de Android View y se integran perfectamente con el paradigma reactivo.

🟩 2. Tipos de Animaciones

  • animate*AsState: Anima valores simples
  • AnimatedVisibility: Mostrar/ocultar con animación
  • AnimatedContent: Transiciones entre contenidos
  • Crossfade: Fundido entre composables
  • updateTransition: Transiciones complejas

🟩 3. Crear el Proyecto

Creamos un nuevo proyecto en Android Studio con Empty Activity y seleccionamos Compose.

🟩 4. Dependencias

📄 build.gradle (Module: app)

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
    implementation 'androidx.activity:activity-compose:1.8.2'
    
    // Compose BOM
    implementation platform('androidx.compose:compose-bom:2023.10.01')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    
    // Animaciones
    implementation 'androidx.compose.animation:animation'
}

🟩 5. MainActivity con Compose

📄 MainActivity.kt

package com.example.composetransitions

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.composetransitions.ui.theme.ComposeTransitionsTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTransitionsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    TransitionsApp()
                }
            }
        }
    }
}

@Composable
fun TransitionsApp() {
    var selectedTab by remember { mutableStateOf(0) }

    Column(modifier = Modifier.fillMaxSize()) {
        TabRow(selectedTabIndex = selectedTab) {
            Tab(
                selected = selectedTab == 0,
                onClick = { selectedTab = 0 },
                text = { Text("Básicas") }
            )
            Tab(
                selected = selectedTab == 1,
                onClick = { selectedTab = 1 },
                text = { Text("Visibilidad") }
            )
            Tab(
                selected = selectedTab == 2,
                onClick = { selectedTab = 2 },
                text = { Text("Contenido") }
            )
        }

        when (selectedTab) {
            0 -> BasicAnimationsScreen()
            1 -> VisibilityAnimationsScreen()
            2 -> ContentAnimationsScreen()
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewTransitionsApp() {
    ComposeTransitionsTheme {
        TransitionsApp()
    }
}

🟩 6. Animaciones Básicas

📄 BasicAnimationsScreen.kt

import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun BasicAnimationsScreen() {
    var expanded by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "Animaciones Básicas",
            style = MaterialTheme.typography.headlineMedium,
            color = Color(0xFF2196F3)
        )

        // Animación de Tamaño
        AnimatedSizeBox(expanded)

        // Animación de Color
        AnimatedColorBox()

        // Animación de Rotación
        AnimatedRotationBox()

        // Botón para cambiar estado
        Button(
            onClick = { expanded = !expanded },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(if (expanded) "Contraer" else "Expandir")
        }
    }
}

@Composable
fun AnimatedSizeBox(expanded: Boolean) {
    val size by animateDpAsState(
        targetValue = if (expanded) 200.dp else 100.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "size"
    )

    Box(
        modifier = Modifier
            .size(size)
            .background(Color(0xFF4CAF50), RoundedCornerShape(16.dp)),
        contentAlignment = Alignment.Center
    ) {
        Text("Tamaño", color = Color.White)
    }
}

@Composable
fun AnimatedColorBox() {
    var colorState by remember { mutableStateOf(true) }

    val color by animateColorAsState(
        targetValue = if (colorState) Color(0xFF2196F3) else Color(0xFFFF5722),
        animationSpec = tween(durationMillis = 1000),
        label = "color"
    )

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(80.dp)
            .background(color, RoundedCornerShape(16.dp))
            .clickable { colorState = !colorState },
        contentAlignment = Alignment.Center
    ) {
        Text("Toca para cambiar color", color = Color.White)
    }
}

@Composable
fun AnimatedRotationBox() {
    var rotated by remember { mutableStateOf(false) }

    val rotation by animateFloatAsState(
        targetValue = if (rotated) 360f else 0f,
        animationSpec = tween(durationMillis = 1000),
        label = "rotation"
    )

    Box(
        modifier = Modifier
            .size(100.dp)
            .rotate(rotation)
            .background(Color(0xFF9C27B0), RoundedCornerShape(16.dp))
            .clickable { rotated = !rotated },
        contentAlignment = Alignment.Center
    ) {
        Text("Rotar", color = Color.White)
    }
}

🟩 7. Animaciones de Visibilidad

📄 VisibilityAnimationsScreen.kt

import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun VisibilityAnimationsScreen() {
    var visible1 by remember { mutableStateOf(false) }
    var visible2 by remember { mutableStateOf(false) }
    var visible3 by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "Animaciones de Visibilidad",
            style = MaterialTheme.typography.headlineMedium,
            color = Color(0xFF2196F3)
        )

        // Fade In/Out
        Button(onClick = { visible1 = !visible1 }) {
            Text("Toggle Fade")
        }

        AnimatedVisibility(
            visible = visible1,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            MessageCard("Fade In/Out", Color(0xFF4CAF50))
        }

        // Slide In/Out
        Button(onClick = { visible2 = !visible2 }) {
            Text("Toggle Slide")
        }

        AnimatedVisibility(
            visible = visible2,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            MessageCard("Slide Vertical", Color(0xFF2196F3))
        }

        // Expand/Collapse
        Button(onClick = { visible3 = !visible3 }) {
            Text("Toggle Expand")
        }

        AnimatedVisibility(
            visible = visible3,
            enter = expandVertically() + fadeIn(),
            exit = shrinkVertically() + fadeOut()
        ) {
            MessageCard("Expand/Collapse", Color(0xFFFF9800))
        }
    }
}

@Composable
fun MessageCard(text: String, color: Color) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(containerColor = color),
        shape = RoundedCornerShape(16.dp)
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(24.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(text, color = Color.White, style = MaterialTheme.typography.titleMedium)
        }
    }
}

🟩 8. Animaciones de Contenido

📄 ContentAnimationsScreen.kt

import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp

@Composable
fun ContentAnimationsScreen() {
    var count by remember { mutableStateOf(0) }
    var selectedIcon by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Animaciones de Contenido",
            style = MaterialTheme.typography.headlineMedium,
            color = Color(0xFF2196F3)
        )

        // AnimatedContent con contador
        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = Color(0xFF4CAF50))
        ) {
            Column(
                modifier = Modifier.padding(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Contador", color = Color.White)
                
                AnimatedContent(
                    targetState = count,
                    transitionSpec = {
                        slideInVertically { it } + fadeIn() with
                        slideOutVertically { -it } + fadeOut()
                    },
                    label = "counter"
                ) { targetCount ->
                    Text(
                        text = "$targetCount",
                        style = MaterialTheme.typography.displayLarge,
                        color = Color.White
                    )
                }

                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Button(onClick = { count-- }) { Text("-") }
                    Button(onClick = { count++ }) { Text("+") }
                    Button(onClick = { count = 0 }) { Text("Reset") }
                }
            }
        }

        // Crossfade con iconos
        val icons = listOf(
            Icons.Default.Home,
            Icons.Default.Favorite,
            Icons.Default.Person,
            Icons.Default.Settings
        )

        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = Color(0xFF2196F3))
        ) {
            Column(
                modifier = Modifier.padding(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Crossfade Icons", color = Color.White)
                
                Crossfade(
                    targetState = icons[selectedIcon],
                    label = "icon"
                ) { icon ->
                    Icon(
                        imageVector = icon,
                        contentDescription = null,
                        modifier = Modifier.size(80.dp),
                        tint = Color.White
                    )
                }

                Spacer(modifier = Modifier.height(16.dp))

                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    icons.forEachIndexed { index, _ ->
                        Button(
                            onClick = { selectedIcon = index },
                            colors = ButtonDefaults.buttonColors(
                                containerColor = if (selectedIcon == index) 
                                    Color(0xFF1976D2) else Color.White
                            )
                        ) {
                            Text("${index + 1}")
                        }
                    }
                }
            }
        }
    }
}

🟩 9. Animación Infinita

@Composable
fun InfiniteAnimationExample() {
    val infiniteTransition = rememberInfiniteTransition(label = "infinite")
    
    val scale by infiniteTransition.animateFloat(
        initialValue = 1f,
        targetValue = 1.5f,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse
        ),
        label = "scale"
    )

    Box(
        modifier = Modifier
            .size(100.dp)
            .scale(scale)
            .background(Color(0xFFFF5722), RoundedCornerShape(50.dp))
    )
}

🟩 10. Especificaciones de Animación

// Tween (lineal con duración)
animationSpec = tween(durationMillis = 1000)

// Spring (efecto rebote)
animationSpec = spring(
    dampingRatio = Spring.DampingRatioMediumBouncy,
    stiffness = Spring.StiffnessLow
)

// Keyframes (animación por cuadros)
animationSpec = keyframes {
    durationMillis = 1000
    0.5f at 500
    0.8f at 800
}

// Repeatable (repetir animación)
animationSpec = repeatable(
    iterations = 3,
    animation = tween(1000),
    repeatMode = RepeatMode.Reverse
)

🟩 11. Tabla de Funciones

FunciónUso
animateDpAsStateAnima Dp
animateColorAsStateAnima Color
animateFloatAsStateAnima Float
AnimatedVisibilityMostrar/ocultar
AnimatedContentCambiar contenido
CrossfadeFundido cruzado
updateTransitionTransiciones complejas

🟩 12. Efectos de Entrada/Salida

EntradaSalida
fadeIn()fadeOut()
slideInHorizontally()slideOutHorizontally()
slideInVertically()slideOutVertically()
expandIn()shrinkOut()
expandHorizontally()shrinkHorizontally()
expandVertically()shrinkVertically()
scaleIn()scaleOut()

▶️ Cómo Ejecutar

  1. Abre Android Studio
  2. Crea proyecto con Compose
  3. Copia el código
  4. Presiona Run
  5. Prueba las diferentes animaciones

🧪 Resultado Final

Aplicación con múltiples tipos de animaciones y transiciones fluidas usando Jetpack Compose.

📥 Descargar Proyecto

👉 

🙌 Gracias por Visitar mi Blog

✔️ Compártelo
✔️ Déjame un comentario
✔️ Sígueme para más contenido

❓ Preguntas Frecuentes

1. ¿Qué es Jetpack Compose?
Framework moderno de UI declarativa que simplifica el desarrollo de interfaces en Android.

2. ¿Son más fáciles las animaciones en Compose?
Sí, Compose hace las animaciones más simples y declarativas que el sistema tradicional de View.

3. ¿Puedo combinar múltiples animaciones?
Sí, usa operadores como + para combinar efectos: slideIn() + fadeIn().

4. ¿Compose reemplaza XML?
Compose es el futuro de UI en Android, pero aún puedes usar XML si lo prefieres.

No hay comentarios:

Publicar un comentario