Saltar al contenido principal
Versión: 0.77

Animaciones

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Las animaciones son cruciales para crear una experiencia de usuario excepcional. Los objetos estáticos deben superar la inercia al comenzar a moverse. Los objetos en movimiento tienen impulso y rara vez se detienen inmediatamente. Las animaciones permiten transmitir movimientos físicamente creíbles en tu interfaz.

React Native ofrece dos sistemas de animación complementarios: Animated para control granular e interactivo de valores específicos, y LayoutAnimation para transacciones animadas de diseño global.

API Animated

La API Animated está diseñada para expresar concisamente diversos patrones de animación e interacción de alto rendimiento. Animated se centra en relaciones declarativas entre entradas y salidas, con transformaciones configurables entre ellas, y métodos start/stop para controlar la ejecución temporal de animaciones.

Animated exporta seis tipos de componentes animables: View, Text, Image, ScrollView, FlatList y SectionList, pero también puedes crear los tuyos usando Animated.createAnimatedComponent().

Por ejemplo, una vista contenedora que aparece con fundido al montarse podría verse así:

Analicemos lo que ocurre aquí. En el método render de FadeInView, se inicializa un nuevo Animated.Value llamado fadeAnim con useRef. La propiedad de opacidad en la View está mapeada a este valor animado. Internamente, se extrae el valor numérico para establecer la opacidad.

Al montarse el componente, la opacidad se establece en 0. Luego, se inicia una animación de suavizado en el valor animado fadeAnim, que actualizará todos sus mapeos dependientes (en este caso, solo la opacidad) en cada fotograma mientras el valor anima hacia el valor final de 1.

Esto se hace de manera optimizada, más rápida que llamar a setState y volver a renderizar. Al ser completamente declarativa, la configuración permite implementar optimizaciones adicionales que serializan la configuración y ejecutan la animación en un hilo de alta prioridad.

Configuración de animaciones

Las animaciones son altamente configurables. Funciones de suavizado personalizadas o predefinidas, retrasos, duraciones, factores de decaimiento, constantes de resorte y más pueden ajustarse según el tipo de animación.

Animated ofrece varios tipos de animación, siendo Animated.timing() el más común. Permite animar un valor en el tiempo usando funciones de suavizado predefinidas o personalizadas. Estas funciones transmiten aceleración y desaceleración graduales de objetos.

Por defecto, timing usa una curva easeInOut que transmite aceleración gradual hasta velocidad máxima y concluye con desaceleración progresiva. Puedes especificar otra función de suavizado mediante el parámetro easing. También se admiten duration personalizada o incluso un delay antes de iniciar.

Por ejemplo, para crear una animación de 2 segundos donde un objeto retrocede ligeramente antes de moverse a su posición final:

tsx
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();

Consulta la sección Configuración de animaciones en la referencia de API Animated para conocer todos los parámetros configurables de las animaciones integradas.

Composición de animaciones

Las animaciones pueden combinarse y ejecutarse en secuencia o paralelo. Las secuenciales pueden iniciarse inmediatamente después de la anterior o con retraso específico. La API Animated proporciona métodos como sequence() y delay(), que reciben arrays de animaciones y llaman automáticamente a start()/stop() según sea necesario.

Por ejemplo, esta animación frena gradualmente, luego rebota con resorte mientras gira en paralelo:

tsx
Animated.sequence([
// decay, then spring to start and twirl
Animated.decay(position, {
// coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // return to start
useNativeDriver: true,
}),
Animated.timing(twirl, {
// and twirl
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // start the sequence group

Si una animación se detiene o interrumpe, todas las demás del grupo también se detienen. Animated.parallel tiene una opción stopTogether que puede establecerse en false para desactivar este comportamiento.

Puedes encontrar la lista completa de métodos de composición en la sección Composición de animaciones de la referencia de la API Animated.

Combinación de valores animados

Puedes combinar dos valores animados mediante suma, multiplicación, división o módulo para crear un nuevo valor animado.

En algunos casos, un valor animado necesita invertir otro valor animado para los cálculos. Un ejemplo es invertir una escala (2x → 0.5x):

tsx
const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();

Interpolación

Cada propiedad puede pasar primero por una interpolación. La interpolación mapea rangos de entrada a rangos de salida, generalmente usando interpolación lineal pero también admite funciones de easing. Por defecto, extrapolará la curva más allá de los rangos dados, pero también puedes configurarla para limitar el valor de salida.

Un mapeo básico para convertir un rango de 0-1 a un rango de 0-100 sería:

tsx
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

Por ejemplo, podrías considerar que tu Animated.Value va de 0 a 1, pero animar la posición desde 150px hasta 0px y la opacidad de 0 a 1. Esto se puede lograr modificando el style del ejemplo anterior así:

tsx
  style={{
opacity: this.state.fadeAnim, // Binds directly
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}

interpolate() también admite múltiples segmentos de rango, lo que es útil para definir zonas muertas y otros trucos. Por ejemplo, para obtener una relación de negación en -300 que llegue a 0 en -100, luego suba a 1 en 0, vuelva a bajar a 0 en 100 y luego una zona muerta que permanezca en 0 para todo lo demás:

tsx
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});

Lo que se mapearía así:

Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0

interpolate() también admite mapeo a cadenas de texto, permitiéndote animar colores y valores con unidades. Por ejemplo, para animar una rotación:

tsx
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});

interpolate() también admite funciones de easing arbitrarias, muchas ya implementadas en el módulo Easing. Además, interpolate() permite configurar el comportamiento de extrapolación del outputRange mediante las opciones extrapolate, extrapolateLeft o extrapolateRight. El valor predeterminado es extend, pero puedes usar clamp para evitar que el valor de salida exceda el outputRange.

Seguimiento de valores dinámicos

Los valores animados también pueden seguir otros valores configurando el toValue de una animación a otro valor animado en lugar de un número plano. Por ejemplo, una animación tipo "Chat Heads" como la usada por Messenger en Android podría implementarse con un spring() vinculado a otro valor animado, o con timing() y duration 0 para seguimiento rígido. También pueden componerse con interpolaciones:

tsx
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
useNativeDriver: true,
}).start();

Los valores animados leader y follower se implementarían usando Animated.ValueXY(). ValueXY es una forma práctica de manejar interacciones 2D, como arrastres o desplazamientos. Es un contenedor básico que incluye dos instancias Animated.Value y funciones auxiliares, haciendo que ValueXY sea reemplazable por Value en muchos casos. Permite rastrear tanto valores x como y en el ejemplo anterior.

Seguimiento de gestos

Los gestos, como arrastres o desplazamientos, y otros eventos pueden mapearse directamente a valores animados usando Animated.event. Esto se hace con una sintaxis de mapa estructurado para extraer valores de objetos de eventos complejos. El primer nivel es un array para permitir mapeo en múltiples argumentos, conteniendo objetos anidados.

Por ejemplo, para mapear event.nativeEvent.contentOffset.x a scrollX (un Animated.Value) en gestos de desplazamiento horizontal:

tsx
 onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}

El siguiente ejemplo implementa un carrusel horizontal donde los indicadores de posición se animan usando Animated.event en el ScrollView:

Ejemplo de ScrollView con Evento Animado

Al usar PanResponder, puedes emplear el siguiente código para extraer las posiciones x e y de gestureState.dx y gestureState.dy. Usamos null en la primera posición del array porque solo nos interesa el segundo argumento pasado al handler de PanResponder, que es el gestureState.

tsx
onPanResponderMove={Animated.event(
[null, // ignore the native event
// extract dx and dy from gestureState
// like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}

Ejemplo de PanResponder con Animated Event

Responder al valor actual de la animación

Puedes notar que no existe una forma clara de leer el valor actual durante una animación. Esto se debe a que el valor solo puede conocerse en tiempo de ejecución nativo debido a optimizaciones. Si necesitas ejecutar JavaScript en respuesta al valor actual, existen dos enfoques:

  • spring.stopAnimation(callback) detendrá la animación e invocará callback con el valor final. Es útil para transiciones gestuales.

  • spring.addListener(callback) invocará callback asincrónicamente durante la animación, proporcionando un valor reciente. Esto es útil para activar cambios de estado, por ejemplo ajustar un elemento a una nueva opción cuando el usuario lo arrastra cerca, ya que estos cambios de estado más grandes son menos sensibles a algunos fotogramas de retraso comparado con gestos continuos como el paneo que deben ejecutarse a 60 fps.

Animated está diseñado para ser completamente serializable, permitiendo que las animaciones se ejecuten de manera eficiente independientemente del ciclo de eventos normal de JavaScript. Esto influye en la API, así que tenlo presente cuando algo parezca más complicado comparado con un sistema completamente síncrono. Consulta Animated.Value.addListener como alternativa para superar algunas limitaciones, pero úsalo con moderación ya que podría tener implicaciones de rendimiento en el futuro.

Usar el controlador nativo (native driver)

La API Animated está diseñada para ser serializable. Al usar el controlador nativo, enviamos toda la información sobre la animación al entorno nativo antes de iniciarla, permitiendo que el código nativo ejecute la animación en el hilo de UI sin pasar por el puente en cada fotograma. Una vez iniciada la animación, el hilo de JS puede bloquearse sin afectarla.

Puedes usar el controlador nativo en animaciones normales configurando useNativeDriver: true al iniciarla. Las animaciones sin la propiedad useNativeDriver usarán false por razones de compatibilidad, pero mostrarán una advertencia (y error de tipado en TypeScript).

tsx
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();

Los valores animados solo son compatibles con un controlador. Si usas el controlador nativo al iniciar una animación en un valor, asegúrate de que todas las animaciones posteriores en ese valor también usen el controlador nativo.

El controlador nativo también funciona con Animated.event. Esto es especialmente útil para animaciones que siguen la posición de desplazamiento, ya que sin el controlador nativo la animación siempre irá un fotograma por detrás del gesto debido a la naturaleza asíncrona de React Native.

tsx
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {y: this.state.animatedValue},
},
},
],
{useNativeDriver: true}, // <-- Set this to true
)}>
{content}
</Animated.ScrollView>

Puedes ver el controlador nativo en acción ejecutando la app RNTester, luego cargando el ejemplo Native Animated. También puedes revisar el código fuente para entender cómo se crearon estos ejemplos.

Advertencias

No todo lo que puedes hacer con Animated es compatible actualmente con el controlador nativo. La principal limitación es que solo puedes animar propiedades no relacionadas con layout: propiedades como transform y opacity funcionarán, pero Flexbox y propiedades de posición no. Al usar Animated.event, solo funciona con eventos directos y no con eventos en burbuja. Esto significa que no funciona con PanResponder pero sí con elementos como ScrollView#onScroll.

Cuando una animación está en curso, puede impedir que los componentes VirtualizedList rendericen más filas. Si necesitas ejecutar animaciones largas o en bucle mientras el usuario desplaza una lista, usa isInteraction: false en la configuración de tu animación para evitar este problema.

Ten en cuenta

Al utilizar estilos de transformación como rotateY, rotateX y otros, asegúrate de incluir el estilo de transformación perspective. Actualmente, algunas animaciones podrían no renderizarse en Android sin este parámetro. Ver ejemplo más abajo.

tsx
<Animated.View
style={{
transform: [
{scale: this.state.scale},
{rotateY: this.state.rotateY},
{perspective: 1000}, // without this line this Animation will not render on Android while working fine on iOS
],
}}
/>

Ejemplos adicionales

La aplicación RNTester incluye varios ejemplos prácticos de Animated:

API LayoutAnimation

LayoutAnimation permite configurar globalmente animaciones de create y update que se aplicarán a todas las vistas en el próximo ciclo de renderizado/layout. Esto es especialmente útil para actualizaciones de diseño Flexbox, evitando tener que medir o calcular propiedades específicas para animarlas directamente. Es particularmente valioso cuando los cambios de diseño afectan elementos ancestros, como una expansión de "ver más" que aumenta el tamaño del contenedor padre y desplaza elementos inferiores, lo que normalmente requeriría coordinación explícita entre componentes para animarlos sincronizadamente.

Aunque LayoutAnimation es muy potente y útil, ofrece menos control que Animated y otras bibliotecas de animación. Si no logras el comportamiento deseado con LayoutAnimation, considera utilizar otro enfoque.

Para que funcione en Android, debes configurar estas flags mediante UIManager:

tsx
UIManager.setLayoutAnimationEnabledExperimental(true);

Este ejemplo utiliza valores predefinidos, pero puedes personalizar las animaciones según tus necesidades. Consulta LayoutAnimation.js para más detalles.

Notas adicionales

requestAnimationFrame

requestAnimationFrame es un polyfill del navegador que quizás conozcas. Acepta una función como único argumento y la ejecuta antes del próximo refresco visual. Es un componente fundamental para animaciones que subyace a todas las APIs de animación basadas en JavaScript. Normalmente no necesitarás llamarlo directamente, ya que las APIs de animación gestionan las actualizaciones de fotogramas automáticamente.

setNativeProps

Como se mencionó en la sección de Manipulación Directa, setNativeProps permite modificar propiedades de componentes nativos (respaldados por vistas nativas, no componentes compuestos) directamente, sin necesidad de usar setState y volver a renderizar la jerarquía de componentes.

Podríamos usar esta técnica en el ejemplo de Rebound para actualizar la escala, especialmente útil si el componente actualizado está profundamente anidado y no ha sido optimizado con shouldComponentUpdate.

Si tus animaciones pierden fotogramas (funcionan por debajo de 60 fps), considera optimizarlas con setNativeProps o shouldComponentUpdate. Otra opción es ejecutar las animaciones en el hilo de UI en lugar del hilo de JavaScript mediante useNativeDriver. También puedes diferir tareas computacionalmente intensas hasta después de las animaciones usando el InteractionManager. Monitorea el rendimiento con la herramienta "FPS Monitor" del menú de desarrollo interno.