Защо 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-handler2.xuseWorkletCallback→ Използвайте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 код продължава да работи без промени.