Animações performáticas no React Native com Reanimated 3

1. Introdução ao Reanimated 3 e o Paradigma das Animações no React Native

Animações performáticas são cruciais para a experiência do usuário em aplicativos mobile. Elas fornecem feedback visual imediato, orientam a navegação e criam uma sensação de fluidez que diferencia apps medianos de experiências premium. No React Native, o sistema de animações nativo enfrenta uma limitação fundamental: a comunicação entre a thread JavaScript (JS) e a thread da UI ocorre através da bridge, que é assíncrona e pode causar atrasos visíveis (jank). Cada quadro de animação precisa ser serializado, enviado pela bridge e processado, resultando em quedas de FPS em animações complexas.

O Reanimated 3 surge como uma solução revolucionária para esse problema. Ele permite que as animações sejam executadas diretamente na thread da UI, eliminando a necessidade de comunicação constante com a thread JS. O conceito central é o worklet — funções JavaScript que são compiladas e executadas nativamente no thread da UI. Isso reduz drasticamente o jank e permite animações a 60 ou até 120 FPS, mesmo em dispositivos de baixo custo. O Reanimated 3 abstrai toda essa complexidade, oferecendo uma API intuitiva para criar animações declarativas e reativas.

2. Instalação, Configuração e Primeiros Passos com Reanimated 3

Para começar, instale o pacote e configure o plugin do Babel:

npm install react-native-reanimated
# ou
yarn add react-native-reanimated

Em seguida, adicione o plugin no arquivo babel.config.js:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['react-native-reanimated/plugin'],
};

A estrutura básica do Reanimated 3 utiliza três elementos principais: useSharedValue, useAnimatedStyle e Animated.View. Um valor compartilhado (useSharedValue) mantém seu estado na thread da UI e pode ser lido/escrito de qualquer worklet. Ao contrário do useState, as alterações em useSharedValue não disparam re-renderizações no componente React — elas atualizam diretamente as propriedades animadas.

Exemplo básico de animação de opacidade:

import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';

function FadeInView({ children }) {
  const opacity = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
  }));

  React.useEffect(() => {
    opacity.value = withTiming(1, { duration: 1000 });
  }, []);

  return <Animated.View style={animatedStyle}>{children}</Animated.View>;
}

3. Trabalhando com Worklets e a Thread da UI

Worklets são funções marcadas com 'worklet' que rodam no thread da UI. Eles têm acesso a valores compartilhados, mas não podem acessar diretamente variáveis do escopo JavaScript — exceto se forem passadas como argumentos ou capturadas via closures especiais. Para comunicação do worklet para a thread JS, usa-se runOnJS.

Exemplo de um worklet que atualiza um estado React:

import { runOnJS } from 'react-native-reanimated';

function GestureHandler() {
  const [gestureState, setGestureState] = React.useState('idle');
  const translateX = useSharedValue(0);

  const updateGestureState = (state) => {
    setGestureState(state);
  };

  const onGestureEvent = useAnimatedGestureHandler({
    onStart: () => {
      runOnJS(updateGestureState)('started');
    },
    onActive: (event) => {
      translateX.value = event.translationX;
    },
    onEnd: () => {
      runOnJS(updateGestureState)('ended');
      translateX.value = withSpring(0);
    },
  });

  // ...
}

4. Animações Baseadas em Gestos com Gesture Handler

A integração entre react-native-gesture-handler e Reanimated 3 é nativa e poderosa. O hook useAnimatedGestureHandler permite processar eventos de gesto diretamente na thread da UI.

Exemplo de um elemento arrastável com snap de retorno:

import { GestureDetector, Gesture } from 'react-native-gesture-handler';

function DraggableBox() {
  const offset = useSharedValue({ x: 0, y: 0 });
  const start = useSharedValue({ x: 0, y: 0 });

  const gesture = Gesture.Pan()
    .onStart(() => {
      start.value = { x: offset.value.x, y: offset.value.y };
    })
    .onUpdate((event) => {
      offset.value = {
        x: start.value.x + event.translationX,
        y: start.value.y + event.translationY,
      };
    })
    .onEnd(() => {
      offset.value = withSpring({ x: 0, y: 0 }); // snap de volta
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: offset.value.x },
      { translateY: offset.value.y },
    ],
  }));

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.box, animatedStyle]} />
    </GestureDetector>
  );
}

5. Animações com Layout e Transições (Layout Animations)

O Reanimated 3 oferece funções de animação como withTiming, withSpring e withSequence. Para animações de entrada e saída, use os componentes FadeIn, SlideInLeft, BounceOut, entre outros.

Exemplo de animação de layout com withSpring:

function AnimatedCard({ expanded }) {
  const height = useSharedValue(100);

  React.useEffect(() => {
    height.value = withSpring(expanded ? 300 : 100, { damping: 15 });
  }, [expanded]);

  const animatedStyle = useAnimatedStyle(() => ({
    height: height.value,
  }));

  return <Animated.View style={[styles.card, animatedStyle]} />;
}

6. Animações Complexas e Performance: Scroll, Carrosséis e Parallax

Para animar listas longas, use Animated.FlatList e useAnimatedScrollHandler. O efeito parallax pode ser implementado sem travamentos, pois os cálculos ocorrem na thread da UI.

Exemplo de parallax em um FlatList:

function ParallaxList() {
  const scrollOffset = useSharedValue(0);

  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollOffset.value = event.contentOffset.y;
    },
  });

  const renderItem = ({ item, index }) => {
    const animatedStyle = useAnimatedStyle(() => ({
      transform: [
        {
          translateY: scrollOffset.value * 0.3 * (index % 2 === 0 ? 1 : -1),
        },
      ],
    }));

    return (
      <Animated.View style={[styles.item, animatedStyle]}>
        <Text>{item.title}</Text>
      </Animated.View>
    );
  };

  return (
    <Animated.FlatList
      data={data}
      renderItem={renderItem}
      onScroll={scrollHandler}
      scrollEventThrottle={16}
    />
  );
}

7. Otimizações Avançadas: Memoização, Reutilização e Debug

Para evitar re-renderizações, use useDerivedValue para derivar valores de outros valores compartilhados. Combine com useMemo para estilos animados em componentes filhos.

Exemplo de otimização:

function OptimizedChild({ sharedValue }) {
  const derivedValue = useDerivedValue(() => {
    return sharedValue.value * 2;
  });

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: derivedValue.value,
  }));

  return <Animated.View style={animatedStyle} />;
}

Para debug, utilize o Flipper com o plugin Reanimated DevTools para inspecionar valores compartilhados e métricas de FPS em tempo real.

8. Casos de Uso Reais e Boas Práticas para Produção

Exemplo completo: animação de like com feedback tátil:

function LikeButton({ onLike }) {
  const scale = useSharedValue(1);
  const isLiked = useSharedValue(false);

  const handlePress = () => {
    scale.value = withSequence(
      withSpring(1.3, { damping: 2 }),
      withSpring(1)
    );
    isLiked.value = !isLiked.value;
    runOnJS(onLike)();
  };

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  return (
    <GestureDetector gesture={Gesture.Tap().onEnd(handlePress)}>
      <Animated.View style={animatedStyle}>
        <Icon name={isLiked.value ? 'heart' : 'heart-outline'} size={32} />
      </Animated.View>
    </GestureDetector>
  );
}

Boas práticas:
- Evite animações na thread JS; mova tudo para worklets.
- Prefira withSpring para gestos, pois oferece resposta mais natural.
- Teste em dispositivos de baixo custo para garantir 60 FPS.
- Use useDerivedValue para evitar cálculos redundantes.

Referências