0
Por qué Reanimated a 60fps
Toda la lógica de animación corre en el UI thread vía JSI — el puente JS nunca bloquea.
JS Thread
React render
State updates
API calls
Declara worklets
Redux / Zustand
JSI
→
UI Thread (nativo)
useSharedValue
useAnimatedStyle
withSpring / withTiming
interpolate
Gesture worklets
Los worklets (funciones marcadas con 'worklet') se compilan al runtime nativo y nunca cruzan el puente — garantía de 60fps.
1
FadeInCard — Resumen Diario
Tarjeta de calorías que aparece con fade + spring al montar el componente.
FadeInCard.tsx
useSharedValue + useAnimatedStyle + withTiming + withSpring
Vista en dispositivo
9:41
Calorías restantes
832 kcal
142g
Proteína
210g
Carbs
48g
Grasa
🌅
Desayuno · 7:30
Avena + banana — 380 kcal
🥗
Comida · 14:15
Ensalada pollo — 520 kcal
↑ fade + spring al montar
// components/FadeInCard.tsx — NutriTrack import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSpring, } from 'react-native-reanimated'; import { useEffect } from 'react'; export function FadeInCard({ children }) { // Valores viven en el UI thread const opacity = useSharedValue(0); const translateY = useSharedValue(20); useEffect(() => { // Timing para fade preciso (600ms) opacity.value = withTiming(1, { duration: 600 }); // Spring para slide natural (sin rebote exagerado) translateY.value = withSpring(0, { damping: 15, stiffness: 120, }); }, []); // Worklet — se ejecuta en UI thread const style = useAnimatedStyle(() => ({ opacity: opacity.value, transform: [{ translateY: translateY.value }], })); return ( <Animated.View style={style} className="bg-green-600 rounded-xl p-4"> {children} </Animated.View> ); }
2
SwipeToLogMeal — Registro por gesto
Desliza un ítem de comida a la izquierda; al 60% del umbral se registra automáticamente con spring snap-back.
SwipeToLogMeal.tsx
Gesture.Pan() + useSharedValue + runOnJS + withSpring
Estado: swipe completado
Sugerencias de hoy
Pollo con arroz
487 kcal
320g
Batido proteico
220 kcal
300ml
Fruta de temporada
95 kcal
180g
← desliza para registrar
spring snap al 60% umbral
// components/SwipeToLogMeal.tsx import { useSharedValue, useAnimatedStyle, withSpring, runOnJS, } from 'react-native-reanimated'; import { Gesture, GestureDetector, } from 'react-native-gesture-handler'; const THRESHOLD = -120; // 60% del ancho de acción export function SwipeToLogMeal({ onLog, children }) { const translateX = useSharedValue(0); const pan = Gesture.Pan() .onUpdate((e) => { // Solo izquierda (negativo) translateX.value = Math.min(0, e.translationX); }) .onEnd((e) => { if (e.translationX < THRESHOLD) { // Registrar — vuelve a JS thread con runOnJS translateX.value = withSpring(-400); runOnJS(onLog)(); } else { // Snap-back con spring natural translateX.value = withSpring(0, { damping: 18, stiffness: 150, }); } }); const style = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], })); return ( <GestureDetector gesture={pan}> <Animated.View style={style}> {children} </Animated.View> </GestureDetector> ); }
3
ParallaxHeaderNutri — Scroll-driven
Cabecera que colapsa con parallax al hacer scroll: altura, opacidad y translateY mapeados vía interpolate.
ParallaxHeaderNutri.tsx
useAnimatedScrollHandler + interpolate + useAnimatedStyle
Scroll = 160px (compacto)
L
Lucía · 832 kcal restantes
Objetivo: 1.800 kcal/día
REGISTRO DE HOY
🌅 Desayuno — Avena
380 kcal
🥗 Comida — Ensalada
520 kcal
☕ Snack — Café con leche
68 kcal
↑ header colapsado al hacer scroll
interpolate → compact mode
// components/ParallaxHeaderNutri.tsx import Animated, { useAnimatedScrollHandler, useSharedValue, useAnimatedStyle, interpolate, } from 'react-native-reanimated'; export function ParallaxHeaderNutri() { const scrollY = useSharedValue(0); // Worklet — corre en UI thread en cada frame const onScroll = useAnimatedScrollHandler({ onScroll: (event) => { scrollY.value = event.contentOffset.y; }, }); // Header: 300 → 80px, opacidad 1 → 0.2 const headerStyle = useAnimatedStyle(() => ({ height: interpolate( scrollY.value, [0, 160], [300, 80] ), opacity: interpolate( scrollY.value, [0, 160], [1, 0.15] ), transform: [{ translateY: interpolate( scrollY.value, [0, 160], [0, -60] ), }], })); return ( <> <Animated.View style={headerStyle} className="bg-green-800"> <NutriHeaderContent /> </Animated.View> <Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}> <MealLog /> </Animated.ScrollView> </> ); }
4
Cómo funciona internamente
Cuatro conceptos clave de Reanimated aplicados en NutriTrack.
useSharedValue
Almacena el estado de animación en el UI thread. Cambios a
.value disparan re-renderizado nativo sin pasar por React.useAnimatedStyle
Worklet que devuelve estilos calculados en cada frame en el UI thread. Sin re-renders de React — pura actualización nativa.
Gesture.Pan()
Los callbacks
onUpdate / onEnd son worklets. El gesto se procesa completamente en el hilo nativo a 120Hz.interpolate
Mapea un rango de entrada (scrollY) a un rango de salida (height, opacity). Worklet puro — 0ms de latencia JS.
5
Reglas para NutriTrack
Checklist de implementación para garantizar 60fps en Snapdragon 665.
Plugin Babel al final
react-native-reanimated/plugin siempre el último en el array de plugins.scrollEventThrottle={16} En ScrollView animados para asegurar eventos a 60fps exactos.
runOnJS para callbacks Usa
runOnJS(onLog)() para volver al hilo JS desde un worklet de gesto.withSpring para gestos Usa damping: 15–20 y stiffness: 120–180 para sensación natural.
No acceder a objetos JS en worklets Solo primitivos y SharedValues — nada de arrays, objetos React ni closures complejos.
Layout animations declarativas Usa
entering/exiting en listas de comidas — Reanimated gestiona el diffing automáticamente.