Este proyecto contiene una colección completa de animaciones en React Native, desde las más básicas hasta las más avanzadas. Perfecto para aprender y experimentar con la API de Animated de React Native.
- ✅ Animaciones básicas (scale, rotate, fade)
- ✅ Animaciones avanzadas (sequence, parallel, loop)
- ✅ Animación de "Now Playing" estilo Spotify
- ✅ Carousel con animaciones de scroll
- ✅ Botones interactivos con feedback visual
- ✅ Integración con enlaces externos
- ✅ OAuth 2.0 con Spotify (Login y gestión de playlists)
# Clonar el repositorio
git clone <tu-repositorio>
cd my-new-app
# Instalar dependencias
npm install
# Ejecutar en iOS
npx expo run:ios
# Ejecutar en Android
npx expo run:android
-
Cuenta de Spotify Developer:
- Ve a Spotify Developer Dashboard
- Inicia sesión con tu cuenta de Spotify
- Crea una nueva aplicación
-
Dependencias necesarias:
npx expo install expo-auth-session expo-crypto expo-web-browser
- Ve a Spotify Developer Dashboard
- Haz clic en "Create App"
- Completa la información:
- App name: Tu nombre de aplicación
- App description: Descripción de tu app
- Website: URL de tu sitio web (opcional)
- Redirect URI:
com.example.ui://
(importante)
- Acepta los términos y crea la aplicación
- En tu aplicación, ve a "Edit Settings"
- En la sección "Redirect URIs", agrega:
com.example.ui://
- Haz clic en "Save"
- En la página de tu aplicación, copia el "Client ID"
- Será algo como:
9a5ca2dbd3d84250aacbde63de954f16
Archivo spotify-config.ts
:
export const SPOTIFY_CONFIG = {
CLIENT_ID: 'TU_CLIENT_ID_AQUI', // Reemplaza con tu Client ID real
REDIRECT_URI: 'com.example.ui://',
SCOPES: [
'user-read-private',
'user-read-email',
'playlist-read-private',
'playlist-read-collaborative',
'user-library-read'
]
};
Archivo app.json
:
{
"expo": {
"scheme": "com.example.ui",
"plugins": [
"expo-web-browser"
]
}
}
npx expo prebuild --clean
interface SpotifyUser {
id: string;
display_name: string;
email: string;
images: Array<{ url: string }>;
}
interface SpotifyPlaylist {
id: string;
name: string;
description: string;
images: Array<{ url: string }>;
tracks: {
total: number;
};
owner: {
display_name: string;
};
}
const handleSpotifyLogin = async () => {
try {
setIsLoading(true);
setError(null);
const authUrl = `https://accounts.spotify.com/authorize?client_id=${SPOTIFY_CLIENT_ID}&response_type=token&redirect_uri=${encodeURIComponent(SPOTIFY_REDIRECT_URI)}&scope=${encodeURIComponent(SPOTIFY_SCOPES)}&show_dialog=true`;
const result = await WebBrowser.openAuthSessionAsync(authUrl, SPOTIFY_REDIRECT_URI);
if (result.type === 'success') {
const url = result.url;
const accessToken = extractAccessToken(url);
if (accessToken) {
await fetchUserProfile(accessToken);
await fetchUserPlaylists(accessToken);
} else {
setError('No se pudo obtener el token de acceso');
}
} else if (result.type === 'cancel') {
setError('Login cancelado por el usuario');
}
} catch (err) {
setError('Error durante el login: ' + err);
} finally {
setIsLoading(false);
}
};
const extractAccessToken = (url: string): string | null => {
const match = url.match(/access_token=([^&]*)/);
return match ? match[1] : null;
};
const fetchUserProfile = async (accessToken: string) => {
try {
const response = await fetch('https://api.spotify.com/v1/me', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (response.ok) {
const userData: SpotifyUser = await response.json();
setUser(userData);
} else {
setError('Error al obtener el perfil del usuario');
}
} catch (err) {
setError('Error al obtener el perfil: ' + err);
}
};
const fetchUserPlaylists = async (accessToken: string) => {
try {
const response = await fetch('https://api.spotify.com/v1/me/playlists?limit=10', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (response.ok) {
const data = await response.json();
setPlaylists(data.items);
} else {
setError('Error al obtener las playlists');
}
} catch (err) {
setError('Error al obtener playlists: ' + err);
}
};
const [user, setUser] = useState<SpotifyUser | null>(null);
const [playlists, setPlaylists] = useState<SpotifyPlaylist[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
{!user ? (
<Pressable style={styles.loginButton} onPress={handleSpotifyLogin} disabled={isLoading}>
<Text style={styles.loginButtonText}>
{isLoading ? 'Conectando...' : 'Conectar con Spotify'}
</Text>
</Pressable>
) : (
// Mostrar información del usuario y playlists
)}
Scope | Descripción |
---|---|
user-read-private |
Leer información privada del usuario |
user-read-email |
Leer email del usuario |
playlist-read-private |
Leer playlists privadas |
playlist-read-collaborative |
Leer playlists colaborativas |
user-library-read |
Leer biblioteca del usuario |
// Estados de error
const [error, setError] = useState<string | null>(null);
// Mostrar errores en la UI
{error && <Text style={styles.errorText}>{error}</Text>}
// Manejo de errores en las funciones
try {
// Código de la función
} catch (err) {
setError('Error descriptivo: ' + err);
}
- Usuario presiona "Conectar con Spotify"
- Se abre navegador con página de autorización de Spotify
- Usuario autoriza la aplicación
- Spotify redirige a la app con el token de acceso
- Se extrae el token de la URL
- Se hacen llamadas a la API de Spotify
- Se muestra la información del usuario y playlists
- Verifica que el Redirect URI en Spotify Dashboard coincida exactamente con
com.example.ui://
- Asegúrate de que el scheme en
app.json
seacom.example.ui
- Verifica que el Client ID en
spotify-config.ts
sea correcto - Asegúrate de que la aplicación esté creada en Spotify Dashboard
- Verifica que el Redirect URI esté configurado correctamente
- Asegúrate de que el usuario haya autorizado la aplicación
- ✅ Login OAuth 2.0 seguro con Spotify
- ✅ Manejo de tokens de acceso
- ✅ Perfil de usuario completo
- ✅ Lista de playlists del usuario
- ✅ Manejo de errores robusto
- ✅ Estados de carga y feedback visual
- ✅ Logout funcional
- ✅ Interfaz moderna con degradados
const heartScale = useRef(new Animated.Value(1)).current;
const animateHeart = () => {
Animated.sequence([
Animated.timing(heartScale, {
toValue: 1.4,
duration: 200,
useNativeDriver: true,
}),
Animated.spring(heartScale, {
toValue: 1,
friction: 3,
tension: 80,
useNativeDriver: true,
})
]).start();
};
Conceptos clave:
Animated.sequence()
: Encadena animacionesAnimated.spring()
: Animación con efecto de resorteuseNativeDriver: true
: Mejora el rendimiento
const animateTwitter = () => {
Animated.sequence([
Animated.timing(twitterScale, {
toValue: 1.4,
duration: 200,
useNativeDriver: true,
}),
Animated.spring(twitterScale, {
toValue: 1,
friction: 3,
tension: 80,
useNativeDriver: true,
})
]).start(() => {
Linking.openURL('https://twitter.com');
});
};
Conceptos clave:
Linking.openURL()
: Abre enlaces externos- Callback en
.start()
: Ejecuta código después de la animación
const bar1Height = useRef(new Animated.Value(10)).current;
const [isPlaying, setIsPlaying] = useState(false);
const startNowPlayingAnimation = () => {
setIsPlaying(true);
const animateBar = (bar: Animated.Value) => {
return Animated.loop(
Animated.sequence([
Animated.timing(bar, {
toValue: Math.random() * 30 + 5,
duration: 300 + Math.random() * 400,
useNativeDriver: false,
}),
Animated.timing(bar, {
toValue: Math.random() * 15 + 5,
duration: 300 + Math.random() * 400,
useNativeDriver: false,
})
])
);
};
Animated.parallel([
animateBar(bar1Height),
animateBar(bar2Height),
animateBar(bar3Height),
animateBar(bar4Height),
animateBar(bar5Height)
]).start();
};
Conceptos clave:
Animated.loop()
: Repite animación infinitamenteAnimated.parallel()
: Ejecuta múltiples animaciones simultáneamenteMath.random()
: Valores aleatorios para simular audio realuseNativeDriver: false
: Necesario para animarheight
const scrollX = useRef(new Animated.Value(0)).current;
// En el ScrollView
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
)}
// Para cada item del carousel
const scale = scrollX.interpolate({
inputRange: [
(i - 1) * (ITEM_WIDTH + ITEM_SPACING),
i * (ITEM_WIDTH + ITEM_SPACING),
(i + 1) * (ITEM_WIDTH + ITEM_SPACING),
],
outputRange: [0.85, 1, 0.85],
extrapolate: 'clamp',
});
Conceptos clave:
Animated.event()
: Conecta eventos nativos con animacionesinterpolate()
: Mapea valores de entrada a valores de salidaextrapolate: 'clamp'
: Limita los valores interpolados
Tipo | Propiedad | Ejemplo |
---|---|---|
Scale | transform: [{ scale: value }] |
Cambiar tamaño |
Rotate | transform: [{ rotate: '45deg' }] |
Rotar elemento |
Translate | transform: [{ translateX: 100 }] |
Mover horizontalmente |
Opacity | opacity: value |
Transparencia |
Método | Descripción | Uso |
---|---|---|
Animated.timing() |
Animación con duración específica | Movimientos suaves |
Animated.spring() |
Animación con efecto de resorte | Botones, feedback |
Animated.decay() |
Animación que se desacelera | Scroll, momentum |
Animated.sequence() |
Encadena múltiples animaciones | Secuencias complejas |
Animated.parallel() |
Ejecuta animaciones simultáneamente | Múltiples elementos |
Animated.loop() |
Repite animación infinitamente | Indicadores de carga |
const styles = StyleSheet.create({
// Contenedor principal
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
// Elementos animados
animatedElement: {
width: 100,
height: 100,
backgroundColor: '#2196F3',
borderRadius: 10,
},
// Botones interactivos
button: {
backgroundColor: '#4CAF50',
padding: 15,
borderRadius: 20,
margin: 10,
},
});
// Combinar estilos base con animaciones
<Animated.View
style={[
styles.animatedElement,
{ transform: [{ scale: animatedValue }] }
]}
/>
// ✅ Usar useNativeDriver cuando sea posible
useNativeDriver: true // Para transform y opacity
useNativeDriver: false // Para height, width, etc.
// ✅ Usar useRef para valores animados
const animatedValue = useRef(new Animated.Value(0)).current;
// ✅ Usar useState para estado de UI
const [isAnimating, setIsAnimating] = useState(false);
// ✅ Detener animaciones al desmontar
useEffect(() => {
return () => {
animatedValue.stopAnimation();
};
}, []);
// ✅ Crear interpolaciones una sola vez
const spin = rotationValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
});
const ButtonWithFeedback = () => {
const scaleValue = useRef(new Animated.Value(1)).current;
const handlePress = () => {
Animated.sequence([
Animated.timing(scaleValue, {
toValue: 0.95,
duration: 100,
useNativeDriver: true,
}),
Animated.spring(scaleValue, {
toValue: 1,
useNativeDriver: true,
})
]).start();
};
return (
<Pressable onPress={handlePress}>
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
<Text>Presionar</Text>
</Animated.View>
</Pressable>
);
};
const LoadingSpinner = () => {
const spinValue = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.loop(
Animated.timing(spinValue, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
})
).start();
}, []);
const spin = spinValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
});
return (
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<Text>⏳</Text>
</Animated.View>
);
};
- Documentación oficial de Animated
- React Native Reanimated
- Lottie para React Native
- Spotify Web API
- Expo Auth Session
- Fork el proyecto
- Crea una rama para tu feature (
git checkout -b feature/AmazingFeature
) - Commit tus cambios (
git commit -m 'Add some AmazingFeature'
) - Push a la rama (
git push origin feature/AmazingFeature
) - Abre un Pull Request
Este proyecto está bajo la Licencia MIT - ver el archivo LICENSE para detalles.
Creado con ❤️ para aprender animaciones en React Native y OAuth 2.0
¡Disfruta creando animaciones increíbles y conectando con Spotify! 🎨✨🎵