Material de consulta. Por qué las animaciones se sienten con tirones, cómo moverlas al hilo nativo, y cuándo NO animar.
Etiquetas
📚 Material de consulta. No ocupa sesión de clase. Está aquí para cuando tu proyecto lo necesite.
Por qué las animaciones importan más en móvil
En una pantalla de teléfono cabe poco y todo cambia rápido. Sin movimiento, el usuario pierde el hilo: ¿esta pantalla es nueva o es la misma?, ¿de dónde salió esta tarjeta?, ¿mi toque hizo algo?
Las animaciones bien usadas no son decoración: son orientación. Le dicen al usuario de dónde viene y a dónde va lo que está viendo. Y tienen un efecto medible en la percepción: una operación con una transición suave se siente más rápida que la misma operación instantánea pero abrupta.
El concepto que lo explica todo: dos hilos
React Native corre tu JavaScript en un hilo y dibuja la interfaz en otro. Esa separación es lo que hace que la app siga respondiendo mientras tu código trabaja… y también la causa de las animaciones con tirones.
Sin
useNativeDriver, cada fotograma de tu animación cruza el puente entre los dos hilos. Sesenta veces por segundo. Si el hilo de JS está ocupado —renderizando una lista, procesando datos—, los fotogramas llegan tarde y el usuario ve tirones.
Con el driver nativo, la animación se entrega completa al lado nativo desde el inicio: JS dice "lleva este valor de 0 a 1 en 300 ms" y se desentiende. La animación sigue fluida aunque el hilo de JS esté saturado.
Por eso la primera tabla es tan corta: solo opacity y transform pueden viajar al hilo nativo. Todo lo demás obliga a recalcular el layout en cada fotograma.
Antes de animar: que las cosas respondan
Si vas a invertir esfuerzo en un solo detalle de movimiento, que sea este: todo lo tocable debe reaccionar de inmediato.
En web existe :hover y el cursor cambia de forma; en móvil no hay nada de eso. El único modo que tiene el usuario de saber que su toque se registró es que algo cambie al instante. El tercer snippet lo resuelve con Pressable y sin ninguna librería — y arregla más de la percepción de calidad de tu app que cualquier animación elaborada.
Gestos, en una nota
Pressable cubre toques. Para gestos reales —deslizar para eliminar, arrastrar, pellizcar para hacer zoom— se usa react-native-gesture-handler, normalmente junto con Reanimated.
La razón de que sean dos bibliotecas separadas y no la API base: un gesto necesita responder al dedo en cada milisegundo, y pasar por el hilo de JS introduce retraso perceptible. Ambas bibliotecas existen para que el gesto y su animación ocurran del lado nativo, sin intermediarios.
Si tu proyecto necesita algo más que toques, ese es el camino. No intentes construir un swipe con PanResponder y Animated sin driver nativo: es el clásico que se siente pesado y no hay forma de arreglarlo después.
Cuándo NO animar
Tres casos donde la animación resta:
- Acciones frecuentes. Una transición de 300 ms es elegante la primera vez y molesta la vigésima. Lo que el usuario hace mucho, que sea instantáneo.
- Contenido crítico. No hagas esperar una animación para mostrar un error o un dato que el usuario está buscando.
- Cuando el sistema pidió menos movimiento. El cuarto snippet muestra cómo detectarlo. Hay personas para quienes el movimiento en pantalla provoca mareo real; el sistema operativo les da un interruptor y tu app debe respetarlo.
La regla general: animaciones cortas (150–300 ms) y con propósito. Si no sabes qué le está comunicando al usuario, probablemente sobra.
Ejemplos de Código
4 ejemplos
Qué se puede animar en el hilo nativo
La lista es corta a propósito.
1✅ CON useNativeDriver (fluidas aunque JS esté ocupado)
2 opacity
3 transform: translateX, translateY, scale, scaleX, scaleY, rotate
4
5❌ SIN driver nativo (recalculan layout, dependen del hilo JS)
6 height, width
7 margin, padding
8 top, left, right, bottom
9 backgroundColor, borderRadius
10
11Regla práctica: si quieres animar tamaño, casi siempre puedes
12lograr el mismo efecto con scale o con translate + overflow hidden.Animación básica de entrada
El patrón de fade + slide que cubre la mayoría de los casos.
1function TarjetaAnimada({ children }) {
2 const progreso = useRef(new Animated.Value(0)).current;
3
4 useEffect(() => {
5 Animated.timing(progreso, {
6 toValue: 1,
7 duration: 250,
8 useNativeDriver: true,
9 }).start();
10 }, []);
11
12 return (
13 <Animated.View
14 style={{
15 opacity: progreso,
16 transform: [{
17 translateY: progreso.interpolate({
18 inputRange: [0, 1],
19 outputRange: [20, 0], // sube 20px al aparecer
20 }),
21 }],
22 }}
23 >
24 {children}
25 </Animated.View>
26 );
27}Feedback táctil — lo mínimo indispensable
Antes de animaciones elaboradas, asegura que todo lo tocable responda.
1// Pressable ya trae estado de presionado sin librerías:
2<Pressable
3 onPress={onPress}
4 style={({ pressed }) => [
5 styles.boton,
6 pressed && { opacity: 0.6, transform: [{ scale: 0.98 }] },
7 ]}
8>
9 <Text>Guardar</Text>
10</Pressable>
11
12Por qué importa: en móvil no hay :hover ni cursor. El ÚNICO modo
13que tiene el usuario de saber que su toque se registró es que algo
14cambie de inmediato. Un botón que no responde al tacto se siente
15roto, aunque funcione.Respetar a quien no quiere movimiento
Requisito de accesibilidad, no detalle opcional.
1import { AccessibilityInfo } from "react-native";
2
3const [reducirMovimiento, setReducir] = useState(false);
4
5useEffect(() => {
6 AccessibilityInfo.isReduceMotionEnabled().then(setReducir);
7 const sub = AccessibilityInfo.addEventListener(
8 "reduceMotionChanged",
9 setReducir
10 );
11 return () => sub.remove();
12}, []);
13
14// Y en la animación:
15duration: reducirMovimiento ? 0 : 300
16
17Hay personas con trastornos vestibulares para quienes el movimiento
18en pantalla provoca mareo real. El sistema operativo les permite
19pedir "menos movimiento"; tu app debe hacerle caso.Recursos
2 recursos disponibles
Reto de Lectura
Reto de Lectura — La animación que da tirones
Se ve fluida en el emulador de tu laptop y a tirones en un teléfono de gama baja. Tres decisiones explican la diferencia.
Este menú desplegable "funciona": se abre y se cierra con animación. En un iPhone reciente se ve bien; en un Android de gama media va a tirones y al scrollear la lista de atrás se congela.
jsx1function MenuDesplegable({ abierto }) { 2 const altura = useRef(new Animated.Value(0)).current; 3 4 useEffect(() => { 5 Animated.timing(altura, { 6 toValue: abierto ? 300 : 0, 7 duration: 300, 8 }).start(); 9 }, [abierto]); 10 11 return ( 12 <Animated.View style={{ height: altura, overflow: "hidden" }}> 13 {opciones.map((o) => ( 14 <Opcion key={o.id} dato={o} /> 15 ))} 16 </Animated.View> 17 ); 18}
Preguntas:
- Falta una propiedad en la configuración de
Animated.timing. ¿Cuál es y qué cambia? - Se está animando
height. ¿Por qué esa propiedad es especialmente cara comparada con mover o escalar? - ¿Qué relación tiene el tirón con el hilo de JavaScript? ¿Por qué empeora justo cuando el usuario scrollea?
- Reescribe la animación para que corra en el hilo nativo.
Documentación Oficial
Documentación de apoyo
Las bibliotecas y guías de esta clase.