Reanimated 4: CSS анимации и преходи в React Native

Практическо ръководство за CSS анимации и преходи в React Native Reanimated 4 — transitions, keyframes, миграция от v3 и примери с код за реални проекти.

Защо Reanimated 4 промени играта за React Native анимациите

Ако някога сте писали анимации в React Native, знаете колко досадно може да стане. Shared values, useAnimatedStyle, worklets… за един прост fade-in или промяна на цвят количеството код беше неоправдано голямо. Честно казано, понякога се чувстваше като да строиш ракета, само за да пуснеш хвърчило.

Е, с пускането на Reanimated 4 през юли 2025 г. нещата се промениха доста сериозно.

Reanimated 4 въвежда CSS анимации и CSS преходи директно в React Native. Анимирате свойства като ширина, цвят и прозрачност чрез обикновени стилове — точно както бихте направили в уеб проект. А най-хубавата част? Всичко продължава да върви на нативния UI thread със стабилни 60+ кадъра в секунда. Без компромиси.

В това ръководство ще разгледаме как работят CSS преходите и keyframe анимациите в Reanimated 4, ще покажем реални примери и ще обясним как да мигрирате от версия 3.x. Хайде да започваме.

Инсталиране и настройка на Reanimated 4

Изисквания

Първо, едно важно уточнение — Reanimated 4 работи само с Новата архитектура на React Native (Fabric). Това означава, че ви трябва:

  • React Native 0.76 или по-нова версия
  • Expo SDK 52+ (ако сте на Expo)
  • Активирана Нова архитектура в проекта

Ако все още сте на старата архитектура (Paper), ще трябва или да мигрирате, или да останете на Reanimated 3.x. Няма трети вариант тук.

Инсталация с Expo

npx expo install react-native-reanimated react-native-worklets

Инсталация с React Native CLI

npm install react-native-reanimated react-native-worklets
cd ios && pod install && cd ..

Конфигурация на Babel

Една промяна, която лесно се пропуска — worklets вече са в отделен пакет. Затова трябва да актуализирате babel.config.js:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // Старо (Reanimated 3.x):
      // 'react-native-reanimated/plugin'
      
      // Ново (Reanimated 4.x):
      'react-native-worklets/plugin'
    ],
  };
};

След промяната задължително рестартирайте Metro bundler с npx expo start --clear или npx react-native start --reset-cache. Иначе ще се чудите защо нищо не работи (говоря от опит).

CSS преходи (Transitions) — анимирайте с промяна на стейт

Как работят CSS преходите

CSS преходите в Reanimated 4 са може би най-значимата нова функционалност. Идеята е елементарна — дефинирате кои свойства да се анимират, задавате продължителност и timing функция, а Reanimated се грижи за плавната интерполация при промяна на стойностите.

Поддържаните свойства за преходи:

  • transitionProperty — масив от свойства за анимиране
  • transitionDuration — продължителност в милисекунди
  • transitionDelay — забавяне преди стартиране
  • transitionTimingFunction — easing функция ('ease-in', 'ease-out', 'ease-in-out', 'linear')
  • transitionBehavior — поведение на прехода

Пример: Разширяващ се контейнер

Нека видим конкретен пример. При натискане контейнерът плавно се разширява и променя цвета си:

import React, { useState } from 'react';
import { Pressable, SafeAreaView, Text } from 'react-native';
import Animated from 'react-native-reanimated';

export default function ExpandableCard() {
  const [expanded, setExpanded] = useState(false);

  return (
    <SafeAreaView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Pressable onPress={() => setExpanded(prev => !prev)}>
        <Animated.View
          style={{
            width: expanded ? 300 : 160,
            height: expanded ? 200 : 100,
            backgroundColor: expanded ? '#6ee7b7' : '#3b82f6',
            borderRadius: 16,
            justifyContent: 'center',
            alignItems: 'center',
            // CSS преходи:
            transitionProperty: ['width', 'height', 'backgroundColor'],
            transitionDuration: 400,
            transitionTimingFunction: 'ease-in-out',
          }}
        >
          <Animated.Text
            style={{
              color: '#fff',
              fontSize: expanded ? 18 : 14,
              fontWeight: 'bold',
              transitionProperty: ['fontSize'],
              transitionDuration: 300,
            }}
          >
            {expanded ? 'Натисни за свиване' : 'Натисни за разширяване'}
          </Animated.Text>
        </Animated.View>
      </Pressable>
    </SafeAreaView>
  );
}

Забележете — няма shared values, няма useAnimatedStyle. Просто променяте стейта с useState и Reanimated автоматично интерполира между старите и новите стойности. Количеството код спада драстично, а резултатът е същият.

Пример: Анимиран бутон с press ефект

Ето и как да направите бутон, който плавно променя фона и мащаба при натискане. Това е от нещата, които се правят постоянно в приложенията:

import React, { useState } from 'react';
import { Pressable, View, Text } from 'react-native';
import Animated from 'react-native-reanimated';

function AnimatedButton({ title, onPress }) {
  const [pressed, setPressed] = useState(false);

  return (
    <Pressable
      onPressIn={() => setPressed(true)}
      onPressOut={() => setPressed(false)}
      onPress={onPress}
    >
      <Animated.View
        style={{
          backgroundColor: pressed ? '#1d4ed8' : '#3b82f6',
          paddingVertical: 14,
          paddingHorizontal: 28,
          borderRadius: 12,
          transform: [{ scale: pressed ? 0.95 : 1 }],
          transitionProperty: ['backgroundColor', 'transform'],
          transitionDuration: 150,
          transitionTimingFunction: 'ease-out',
        }}
      >
        <Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>
          {title}
        </Text>
      </Animated.View>
    </Pressable>
  );
}

150 милисекунди продължителност е точно колкото трябва — достатъчно бързо за натискане, но усеща се визуално.

Keyframe анимации — цикли, пулсации и зареждане

Какво представляват keyframe анимациите

Keyframe анимациите ви позволяват да дефинирате многостъпкови анимации, които могат да се повтарят безкрайно. Те са идеални за индикатори за зареждане, пулсиращи елементи и всякакви визуални акценти, които трябва да вървят на заден план.

Ето поддържаните свойства:

  • animationName — обект с keyframe дефиниция
  • animationDuration — продължителност на един цикъл
  • animationIterationCount — брой повторения (или Infinity)
  • animationDelay — забавяне преди стартиране
  • animationDirection — посока ('normal', 'reverse', 'alternate')
  • animationFillMode — крайно състояние ('forwards', 'backwards', 'both')
  • animationTimingFunction — easing функция

Пример: Пулсиращ индикатор

import React from 'react';
import { View } from 'react-native';
import Animated from 'react-native-reanimated';

const pulseKeyframes = {
  from: {
    transform: [{ scale: 1 }],
    opacity: 1,
  },
  '50%': {
    transform: [{ scale: 1.15 }],
    opacity: 0.7,
  },
  to: {
    transform: [{ scale: 1 }],
    opacity: 1,
  },
};

export default function PulsingDot() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Animated.View
        style={{
          width: 24,
          height: 24,
          borderRadius: 12,
          backgroundColor: '#ef4444',
          animationName: pulseKeyframes,
          animationDuration: 1500,
          animationIterationCount: Infinity,
          animationTimingFunction: 'ease-in-out',
        }}
      />
    </View>
  );
}

Кратко и ясно. Никакви допълнителни hook-ове, никакви worklet директиви.

Пример: Skeleton Loading ефект

Shimmer ефектът за зареждане е навсякъде днес — от социалните мрежи до банковите приложения. Ето как се имплементира с keyframe анимации в Reanimated 4:

import React from 'react';
import { View, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';

const shimmerKeyframes = {
  from: {
    opacity: 0.4,
  },
  '50%': {
    opacity: 0.8,
  },
  to: {
    opacity: 0.4,
  },
};

function SkeletonLine({ width, height = 16, marginBottom = 8 }) {
  return (
    <Animated.View
      style={{
        width,
        height,
        borderRadius: 8,
        backgroundColor: '#e5e7eb',
        marginBottom,
        animationName: shimmerKeyframes,
        animationDuration: 1200,
        animationIterationCount: Infinity,
        animationTimingFunction: 'ease-in-out',
      }}
    />
  );
}

export default function SkeletonCard() {
  return (
    <View style={styles.card}>
      <SkeletonLine width="60%" height={20} />
      <SkeletonLine width="100%" />
      <SkeletonLine width="100%" />
      <SkeletonLine width="80%" />
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    padding: 16,
    backgroundColor: '#fff',
    borderRadius: 12,
    margin: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 3,
  },
});

Пример: Завъртащ се спинер

Класиката. Всяко приложение има нужда от спинер:

import React from 'react';
import { View } from 'react-native';
import Animated from 'react-native-reanimated';

const spinKeyframes = {
  from: {
    transform: [{ rotate: '0deg' }],
  },
  to: {
    transform: [{ rotate: '360deg' }],
  },
};

export default function Spinner({ size = 40, color = '#3b82f6' }) {
  return (
    <View style={{ alignItems: 'center', justifyContent: 'center' }}>
      <Animated.View
        style={{
          width: size,
          height: size,
          borderRadius: size / 2,
          borderWidth: 3,
          borderColor: '#e5e7eb',
          borderTopColor: color,
          animationName: spinKeyframes,
          animationDuration: 800,
          animationIterationCount: Infinity,
          animationTimingFunction: 'linear',
        }}
      />
    </View>
  );
}

Десет реда стил и имате спинер, който върти на 60fps. Преди щяхте да пишете три пъти повече код за същия резултат.

Готови пресети с react-native-css-animations

Ако не ви се пишат keyframes ръчно всеки път (а на кого му се пише?), екипът на Software Mansion предлага библиотека с готови CSS анимационни пресети, вдъхновени от Tailwind CSS:

npm install react-native-css-animations

Ето колко просто става:

import React from 'react';
import { View } from 'react-native';
import Animated from 'react-native-reanimated';
import { spin, pulse, bounce } from 'react-native-css-animations';

export default function PresetExamples() {
  return (
    <View style={{ flexDirection: 'row', gap: 24, padding: 24 }}>
      {/* Въртящ се спинер */}
      <Animated.View
        style={[
          {
            width: 40,
            height: 40,
            borderRadius: 20,
            borderWidth: 3,
            borderColor: '#e5e7eb',
            borderTopColor: '#3b82f6',
          },
          spin,
        ]}
      />

      {/* Пулсиращ елемент */}
      <Animated.View
        style={[
          {
            width: 40,
            height: 40,
            borderRadius: 20,
            backgroundColor: '#ef4444',
          },
          pulse,
        ]}
      />

      {/* Подскачащ елемент */}
      <Animated.View
        style={[
          {
            width: 40,
            height: 40,
            borderRadius: 8,
            backgroundColor: '#10b981',
          },
          bounce,
        ]}
      />
    </View>
  );
}

Библиотеката е лека и включва най-използваните анимации — spin, pulse, bounce, ping и shimmer. Перфектни за бързо прототипиране, когато не ви трябва нещо custom.

Кога да използвате CSS анимации и кога worklets

Reanimated 4 ви дава два подхода за анимации и двата си имат място. Ето кратко ориентиране кога кой е по-подходящ.

CSS преходи и keyframes са по-добрият избор когато:

  • Анимирате прости промени на стойности (цвят, размер, прозрачност)
  • Имате предсказуеми начални и крайни състояния
  • Създавате индикатори за зареждане и циклични ефекти
  • Анимирате появяване и изчезване на модали, тултипове, нотификации
  • Правите разширяване/свиване на акордеони и dropdown менюта
  • Искате по-малко код и по-бърза разработка

Worklets и shared values остават по-добрият избор когато:

  • Имате gesture-driven анимации (swipe-to-delete, drag-and-drop)
  • Трябва да реагирате на scroll позиция (parallax, sticky headers)
  • Нуждаете се от контрол кадър по кадър
  • Анимацията зависи от скорост или физика (withDecay, withSpring)
  • Имплементирате сложни интерактивни елементи (bottom sheets с жестове)

По моя опит, за повечето стандартни UI анимации в типично приложение CSS подходът е напълно достатъчен. Worklets остават незаменими за интерактивни жестове и scroll-базирани ефекти — но за останалото вече не са нужни.

Комбиниране на CSS анимации с Gesture Handler

Хубавото е, че двата подхода се комбинират без никакви проблеми. Ето пример за карта, която използва CSS преходи за визуален фийдбек и worklets за drag жест:

import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

export default function DraggableCard() {
  const [isActive, setIsActive] = useState(false);
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  const drag = Gesture.Pan()
    .onStart(() => {
      // runOnJS за актуализиране на React стейт от UI thread
      'worklet';
      // Активираме CSS прехода за фон
    })
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    })
    .onTouchesDown(() => {
      setIsActive(true);
    })
    .onFinalize(() => {
      setIsActive(false);
    });

  const animatedDragStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));

  return (
    <View style={styles.container}>
      <GestureDetector gesture={drag}>
        <Animated.View
          style={[
            styles.card,
            animatedDragStyle,
            {
              // CSS преходи за фон и сянка
              backgroundColor: isActive ? '#dbeafe' : '#ffffff',
              shadowOpacity: isActive ? 0.25 : 0.1,
              transitionProperty: ['backgroundColor', 'shadowOpacity'],
              transitionDuration: 200,
              transitionTimingFunction: 'ease-out',
            },
          ]}
        >
          <Text style={styles.cardTitle}>Плъзнете ме</Text>
          <Text style={styles.cardText}>
            Тази карта използва worklets за жест и CSS преходи за визуален ефект
          </Text>
        </Animated.View>
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  card: {
    width: 280,
    padding: 20,
    borderRadius: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowRadius: 12,
    elevation: 5,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  cardText: {
    fontSize: 14,
    color: '#6b7280',
  },
});

Работи изненадващо добре. CSS преходите се грижат за визуалната обратна връзка (промяна на цвят и сянка), а worklets управляват позицията при drag. Всеки инструмент прави това, в което е най-добър.

Промени в spring анимациите (withSpring)

Reanimated 4 промени и spring анимациите по начин, който може да ви хване неподготвени, ако не знаете за промените.

Нов параметър energyThreshold

Старите параметри restDisplacementThreshold и restSpeedThreshold са премахнати. На тяхно място идва единственият energyThreshold. Добрата новина е, че е релативен — в повечето случаи просто премахнете старите параметри и стойността по подразбиране ще свърши работа.

Перцептуална продължителност

Тук има един нюанс. Параметърът duration вече означава перцептуална продължителност — времето, в което анимацията изглежда завършена за потребителя. Реалната продължителност е приблизително 1.5 пъти по-дълга.

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

// Reanimated 4 — перцептуалната продължителност
// 200ms перцептуално ≈ 300ms реално
const springAnimation = withSpring(100, {
  duration: 200,
  dampingRatio: 0.7,
});

// Ако искате да запазите старото поведение от v3:
import { Reanimated3DefaultSpringConfig } from 'react-native-reanimated';
const legacySpring = withSpring(100, Reanimated3DefaultSpringConfig);

Препоръката от екипа е да използвате duration и dampingRatio за по-предсказуемо поведение. Така анимацията завършва в зададеното време, независимо от разстоянието.

Миграция от Reanimated 3.x към 4.x

Добрата новина е, че миграцията не е толкова страшна, колкото звучи. Ето стъпките една по една.

Стъпка 1: Инсталирайте новите пакети

npx expo install react-native-reanimated@latest react-native-worklets

Стъпка 2: Актуализирайте Babel плъгина

// babel.config.js
// Преди:
plugins: ['react-native-reanimated/plugin']

// След:
plugins: ['react-native-worklets/plugin']

Стъпка 3: Заменете премахнати API-та

  • useAnimatedGestureHandler → Използвайте Gesture API от react-native-gesture-handler 2.x
  • useWorkletCallback → Използвайте useCallback с 'worklet' директива
  • combineTransition → Използвайте EntryExitTransition.entering().exiting()

Стъпка 4: Актуализирайте spring параметрите

Премахнете restDisplacementThreshold и restSpeedThreshold — стойностите по подразбиране на energyThreshold са напълно достатъчни за повечето случаи.

Стъпка 5: Проверете зависимите библиотеки

Ако използвате @gorhom/react-native-bottom-sheet, актуализирайте поне до версия 5.1.8. Повечето популярни библиотеки вече поддържат Reanimated 4, но си струва да проверите.

Стъпка 6: Добавете CSS анимации постепенно

Не е нужно да мигрирате всичко наведнъж — и всъщност не бих го препоръчал. Съществуващият код с worklets работи без промени. Добавяйте CSS анимации при нови компоненти или когато рефакторирате стари.

Практически съвети за производителност

CSS анимациите в Reanimated 4 са оптимизирани за UI thread, но има няколко неща, които да имате предвид:

  • Анимирайте предимно transform и opacity — тези свойства не предизвикват re-layout и са най-бързите за рендериране
  • Внимавайте с layout свойства в списъци — анимиране на width, height или padding в дълги списъци е тежко. За тези случаи по-добре използвайте transform: scale
  • Не анимирайте повече от необходимото — всяко допълнително свойство добавя работа на UI thread
  • Тествайте на реални устройства — емулаторите не показват реалната производителност. Сериозно, не ги вярвайте за анимации

Често задавани въпроси

Мога ли да използвам Reanimated 4 със старата архитектура?

Не. Reanimated 4 изисква Новата архитектура (Fabric) и React Native 0.76+. Ако все още сте на Paper, ще трябва или да мигрирате, или да останете на Reanimated 3.x. С Expo SDK 53 Новата архитектура е активирана по подразбиране, така че ако сте на Expo, вероятно вече сте готови.

Каква е разликата между CSS преходи и keyframe анимации?

CSS преходите анимират плавно между две стойности при промяна на стейт — идеални за toggle ефекти, промяна на цвят или размер. Keyframe анимациите дефинират многостъпкови анимации, които могат да се повтарят — перфектни за индикатори за зареждане и безкрайни цикли.

Работят ли на 60 fps?

Да. Въпреки простия CSS-подобен синтаксис, всички анимации се изпълняват на нативния UI thread. Получавате стабилни 60+ fps дори когато JavaScript thread е зает.

Трябва ли да пренапиша всичките си worklet анимации?

Не, и не бива. Reanimated 4 е напълно обратно съвместим. Добавяйте CSS анимации постепенно за нови компоненти и прости случаи. Запазете worklets за gesture-driven и scroll-базирани анимации, където наистина блестят.

Как да мигрирам без да счупя приложението?

Инсталирайте react-native-worklets, сменете Babel плъгина, премахнете deprecated API-та (като useAnimatedGestureHandler и restDisplacementThreshold) и актуализирайте зависимите библиотеки. Съществуващият worklet код продължава да работи без промени.

За Автора Editorial Team

Our team of expert writers and editors.