Введение: почему анимации так важны в мобильных приложениях
Давайте начистоту: анимации — это не просто «красивости» в интерфейсе. Они буквально определяют, будет ли ваше приложение казаться качественным или любительским. Плавный переход между экранами, отклик элемента на жест пользователя, визуальная обратная связь при нажатии кнопки — всё это формирует ощущение продукта, за который не стыдно. В 2026 году пользователи ожидают анимации на уровне 60–120 fps, и любое «подтормаживание» моментально воспринимается как баг.
Экосистема анимаций в React Native проделала огромный путь за последние годы. От базового Animated API, который страдал от ограничений JavaScript-потока, до современного Reanimated 4, выполняющего анимации целиком на UI-потоке с нативной производительностью. С выходом Reanimated 4.2.1 и обязательным переходом на New Architecture мы получили ещё более мощные инструменты: CSS-анимации и переходы прямо в React Native, улучшенную интеграцию с Gesture Handler 2 и новый подход к управлению воркалетами через отдельный пакет react-native-worklets.
В этом руководстве я постарался охватить всю экосистему анимаций React Native в 2026 году — от базовых концепций до продвинутых техник, от установки до создания сложных интерактивных элементов с жестами. Каждый раздел содержит рабочие примеры кода, которые можно брать и использовать в своих проектах.
Обзор экосистемы анимаций React Native
Прежде чем углубляться в детали Reanimated 4, стоит кратко рассмотреть, какие вообще инструменты доступны для анимаций в React Native.
Встроенный Animated API
React Native поставляется со встроенным модулем Animated, который даёт базовые возможности для создания анимаций. Он поддерживает примитивы вроде Animated.timing, Animated.spring и Animated.decay. Но его главный минус — большинство операций проходят через JavaScript-поток, что приводит к просадкам при сложных анимациях. Да, есть useNativeDriver: true, но он ограничен только трансформациями и прозрачностью — а этого часто недостаточно.
Reanimated — индустриальный стандарт
React Native Reanimated стал де-факто стандартом для анимаций в React Native. Начиная с версии 2, библиотека использует концепцию воркалетов (worklets) — функций, которые выполняются непосредственно на UI-потоке. Это позволяет достигать стабильных 60–120 fps даже при сложных анимациях с жестами.
Версия 4.x (текущая — 4.2.1) принесла революционные изменения: поддержку CSS-анимаций, обязательную New Architecture и вынос воркалетов в отдельный пакет.
Другие библиотеки
Moti — декларативная обёртка над Reanimated от Фернандо Рохо. Упрощает создание анимаций с помощью простых пропсов вроде from и animate. Отличный выбор, если хочется быстро добавить анимации без глубокого погружения в Reanimated API.
React Native Skia используется для сложных графических анимаций, рисования и шейдеров. В связке с Reanimated позволяет создавать впечатляющие визуальные эффекты.
Lottie React Native по-прежнему популярен для воспроизведения анимаций из After Effects, экспортированных в JSON. Идеально подходит для иллюстративных анимаций — загрузочные экраны, анимированные иконки и тому подобное.
Установка и настройка Reanimated 4
Reanimated 4 требует New Architecture — это обязательное условие, без вариантов. Убедитесь, что ваш проект настроен для работы с New Architecture, прежде чем устанавливать библиотеку.
Установка с Expo SDK 55
Expo SDK 55 использует React Native 0.83 и по умолчанию включает New Architecture, что делает интеграцию с Reanimated 4 максимально простой:
npx expo install react-native-reanimated react-native-worklets
Ключевое изменение в Reanimated 4 — воркалеты были вынесены в отдельный пакет react-native-worklets. Соответственно, и конфигурация Babel-плагина изменилась (об этом чуть ниже).
Установка в bare React Native
npm install react-native-reanimated@^4.2.1 react-native-worklets
# или
yarn add react-native-reanimated@^4.2.1 react-native-worklets
Для iOS не забудьте установить поды:
cd ios && pod install && cd ..
Конфигурация Babel
Обратите внимание на важнейшее изменение: плагин Babel теперь берётся из пакета react-native-worklets, а не из react-native-reanimated, как было в версии 3. Честно говоря, на этом моменте многие спотыкаются при обновлении — так что обновите ваш babel.config.js:
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'], // или 'module:@react-native/babel-preset'
plugins: [
// ВАЖНО: В Reanimated 4 плагин переехал в react-native-worklets
// Раньше было: 'react-native-reanimated/plugin'
// Теперь:
'react-native-worklets/plugin',
],
};
};
После изменения конфигурации Babel обязательно очистите кеш — иначе рискуете потратить час на отладку несуществующих ошибок:
# Для Expo
npx expo start --clear
# Для bare React Native
npx react-native start --reset-cache
Проверка установки
Создайте простой компонент для проверки, что Reanimated работает корректно:
import React from 'react';
import { View, Text } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
export default function ReanimatedTest() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Animated.View
style={[
{ width: 100, height: 100, backgroundColor: '#6C63FF', borderRadius: 16 },
animatedStyle,
]}
onTouchStart={() => {
offset.value = withSpring(offset.value === 0 ? 150 : 0);
}}
/>
<Text style={{ marginTop: 20 }}>Нажмите на квадрат</Text>
</View>
);
}
Если квадрат плавно сдвигается при нажатии — значит, всё настроено правильно.
Основы Reanimated: useSharedValue и useAnimatedStyle
Ядро Reanimated строится на двух фундаментальных концепциях: shared values и animated styles. Без понимания этих концепций дальше двигаться будет сложно.
useSharedValue — значения на UI-потоке
Shared values — это специальные реактивные значения, которые живут на UI-потоке. В отличие от привычного useState, изменение shared value не вызывает перерендер компонента. Значение обновляется напрямую на UI-потоке, что позволяет анимациям работать на 60–120 fps, не затрагивая JavaScript-поток.
import { useSharedValue } from 'react-native-reanimated';
function MyComponent() {
// Создаём shared value с начальным значением 0
const translateX = useSharedValue(0);
const opacity = useSharedValue(1);
const scale = useSharedValue(1);
// Изменение значения — мгновенное, без анимации
translateX.value = 100;
// Изменение значения с анимацией
translateX.value = withSpring(100);
opacity.value = withTiming(0, { duration: 500 });
}
useAnimatedStyle — связывание значений со стилями
Хук useAnimatedStyle создаёт стилевой объект, который автоматически обновляется при изменении shared values. Функция, переданная в этот хук, выполняется как воркалет на UI-потоке — и это принципиально важно для производительности.
import React from 'react';
import { View, Button } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
Easing,
} from 'react-native-reanimated';
export default function AnimatedBox() {
const translateY = useSharedValue(0);
const borderRadius = useSharedValue(16);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
borderRadius: borderRadius.value,
}));
const handlePress = () => {
translateY.value = withSpring(translateY.value === 0 ? -150 : 0, {
damping: 12,
stiffness: 90,
});
borderRadius.value = withTiming(
borderRadius.value === 16 ? 50 : 16,
{ duration: 400, easing: Easing.bezier(0.25, 0.1, 0.25, 1) }
);
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Animated.View
style={[
{ width: 100, height: 100, backgroundColor: '#6C63FF' },
animatedStyle,
]}
/>
<Button title="Анимировать" onPress={handlePress} />
</View>
);
}
useAnimatedProps — анимация нестилевых пропсов
Для анимации пропсов, не связанных со стилями (например, SVG-атрибутов), есть useAnimatedProps:
import Animated, {
useSharedValue,
useAnimatedProps,
withRepeat,
withTiming,
} from 'react-native-reanimated';
import { Circle, Svg } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export default function PulsatingCircle() {
const radius = useSharedValue(30);
React.useEffect(() => {
radius.value = withRepeat(
withTiming(50, { duration: 1000 }),
-1,
true
);
}, []);
const animatedProps = useAnimatedProps(() => ({
r: radius.value,
}));
return (
<Svg height="120" width="120" viewBox="0 0 120 120">
<AnimatedCircle
cx="60"
cy="60"
fill="#6C63FF"
animatedProps={animatedProps}
/>
</Svg>
);
}
Функции анимации: withTiming, withSpring, withDecay
Reanimated предоставляет три основных функции для создания анимаций, и каждая моделирует свой тип движения. Разберём их подробно.
withTiming — анимация на основе времени
withTiming создаёт анимацию с фиксированной продолжительностью. Это самый предсказуемый тип — идеально подходит для переходов интерфейса, изменения прозрачности и цветов.
import { withTiming, Easing } from 'react-native-reanimated';
// Простая анимация с длительностью по умолчанию (300мс)
opacity.value = withTiming(0);
// С указанием длительности
translateX.value = withTiming(200, { duration: 600 });
// С пользовательской кривой ускорения
scale.value = withTiming(1.5, {
duration: 800,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
// С обратным вызовом по завершении
rotation.value = withTiming(360, { duration: 1000 }, (finished) => {
if (finished) {
console.log('Анимация завершена!');
}
});
// Доступные кривые ускорения (Easing):
// Easing.linear — равномерное движение
// Easing.ease — стандартная кривая (по умолчанию)
// Easing.in(Easing.quad) — медленный старт
// Easing.out(Easing.quad) — медленная остановка
// Easing.inOut(Easing.cubic) — плавный старт и остановка
// Easing.bezier(x1, y1, x2, y2) — кубическая кривая Безье
withSpring — пружинная анимация
withSpring моделирует физику пружины. По моему опыту, результат выглядит гораздо естественнее, чем у withTiming — объект «пружинит» вокруг целевого значения, и это создаёт ощущение «живого» интерфейса. Длительность здесь определяется физическими параметрами, а не фиксированным значением.
import { withSpring } from 'react-native-reanimated';
// Пружинная анимация с параметрами по умолчанию
translateX.value = withSpring(100);
// С настройкой физических параметров
translateY.value = withSpring(200, {
mass: 1, // масса объекта (больше = медленнее, инерционнее)
damping: 10, // сила торможения (больше = меньше колебаний)
stiffness: 100, // жёсткость пружины (больше = быстрее, резче)
overshootClamping: false, // если true, пружина не перелетает за цель
restDisplacementThreshold: 0.01,
restSpeedThreshold: 2,
});
// «Мягкая» пружина — плавное, медленное движение
scale.value = withSpring(1.2, {
mass: 2,
damping: 15,
stiffness: 50,
});
// «Жёсткая» пружина — быстрое, упругое движение
scale.value = withSpring(1.2, {
mass: 0.5,
damping: 6,
stiffness: 200,
});
withDecay — анимация инерции
withDecay создаёт анимацию, которая начинается с определённой скорости и постепенно замедляется. Идеально для имитации инерционной прокрутки или движения после жеста — вы буквально «отпускаете» элемент, и он продолжает двигаться по инерции.
import { withDecay } from 'react-native-reanimated';
// Инерция с начальной скоростью
translateX.value = withDecay({
velocity: 800,
deceleration: 0.998,
});
// С ограничением области движения
translateX.value = withDecay({
velocity: velocityFromGesture,
deceleration: 0.997,
clamp: [-200, 200],
});
// С «упругими» границами
translateX.value = withDecay({
velocity: velocityFromGesture,
deceleration: 0.998,
clamp: [-200, 200],
rubberBandEffect: true,
rubberBandFactor: 0.6,
});
Комбинирование анимаций: withSequence, withRepeat, withDelay
А вот где начинается настоящее веселье — комбинирование базовых функций анимации:
import {
withSequence,
withRepeat,
withDelay,
withTiming,
withSpring,
} from 'react-native-reanimated';
// Последовательность анимаций
scale.value = withSequence(
withTiming(1.2, { duration: 200 }),
withTiming(0.9, { duration: 150 }),
withSpring(1)
);
// Повторяющаяся анимация
rotation.value = withRepeat(
withTiming(360, { duration: 2000 }),
-1, // -1 = бесконечно
false // false = не реверсировать
);
// Пульсация (повтор с реверсом)
opacity.value = withRepeat(
withTiming(0.3, { duration: 800 }),
-1,
true // true = реверс (туда-обратно)
);
// Задержка перед анимацией
translateY.value = withDelay(
500,
withSpring(100)
);
CSS-анимации и переходы в Reanimated 4
Пожалуй, самое значительное нововведение Reanimated 4 — поддержка CSS-анимаций и переходов. Если вы знакомы с CSS-анимациями в веб-разработке, то почувствуете себя как дома. Этот API позволяет описывать анимации декларативно, без явного управления shared values.
CSS-переходы (transitions)
CSS-переходы позволяют плавно анимировать изменение стилей. Просто указываете, какие свойства должны анимироваться, и при каждом изменении значения происходит плавный переход. Никаких shared values, никаких хуков — минимум кода:
import React, { useState } from 'react';
import { View, Button } from 'react-native';
import Animated from 'react-native-reanimated';
export default function CSSTransitionExample() {
const [expanded, setExpanded] = useState(false);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Animated.View
style={{
width: expanded ? 250 : 100,
height: expanded ? 250 : 100,
backgroundColor: expanded ? '#FF6B6B' : '#6C63FF',
borderRadius: expanded ? 125 : 16,
transitionProperty: 'width, height, backgroundColor, borderRadius',
transitionDuration: '0.5s',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
}}
/>
<Button
title={expanded ? 'Свернуть' : 'Развернуть'}
onPress={() => setExpanded(!expanded)}
/>
</View>
);
}
Основные свойства CSS-переходов:
- transitionProperty — список свойств для анимации (через запятую)
- transitionDuration — длительность перехода ('0.3s' или '300ms')
- transitionTimingFunction — кривая ускорения ('ease', 'linear', 'ease-in-out', 'cubic-bezier(...)')
CSS-анимации с ключевыми кадрами (keyframes)
Для более сложных анимаций Reanimated 4 поддерживает ключевые кадры, аналогичные @keyframes в CSS. Можно определять многоступенчатые анимации с промежуточными состояниями:
import React from 'react';
import { View } from 'react-native';
import Animated from 'react-native-reanimated';
const bounceKeyframes = {
'0%': { transform: [{ translateY: 0 }] },
'25%': { transform: [{ translateY: -40 }] },
'50%': { transform: [{ translateY: 0 }] },
'75%': { transform: [{ translateY: -20 }] },
'100%': { transform: [{ translateY: 0 }] },
};
const pulseKeyframes = {
'0%': { opacity: 1, transform: [{ scale: 1 }] },
'50%': { opacity: 0.6, transform: [{ scale: 1.15 }] },
'100%': { opacity: 1, transform: [{ scale: 1 }] },
};
export default function CSSKeyframesExample() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', gap: 40 }}>
<Animated.View
style={{
width: 80,
height: 80,
backgroundColor: '#6C63FF',
borderRadius: 40,
animationName: bounceKeyframes,
animationDuration: '2s',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
}}
/>
<Animated.View
style={{
width: 80,
height: 80,
backgroundColor: '#FF6B6B',
borderRadius: 16,
animationName: pulseKeyframes,
animationDuration: '1.5s',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
}}
/>
</View>
);
}
Свойства CSS-анимаций:
- animationName — объект ключевых кадров
- animationDuration — длительность одного цикла
- animationTimingFunction — кривая ускорения
- animationIterationCount — количество повторений ('infinite' или число)
Когда использовать CSS-анимации, а когда воркалеты
CSS-анимации идеально подходят для простых декларативных вещей: загрузочные индикаторы, пульсации, переходы состояний кнопок, анимации появления. Они проще в написании и не требуют явной работы с shared values.
А вот воркалеты и связка useSharedValue + useAnimatedStyle остаются лучшим выбором для интерактивных анимаций с жестами, для сложной логики и для случаев, когда нужен максимальный контроль. Проще говоря: если анимация реагирует на палец пользователя — используйте воркалеты.
Layout-анимации: entering и exiting
Layout-анимации — одна из моих любимых возможностей Reanimated. Они позволяют автоматически анимировать появление, исчезновение и перемещение элементов в макете без ручного управления анимацией. Просто добавляете пропс — и всё работает.
Анимации появления (entering)
import React, { useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import Animated, {
FadeIn,
FadeInDown,
SlideInRight,
BounceIn,
ZoomIn,
FlipInEasyX,
} from 'react-native-reanimated';
export default function EnteringAnimations() {
const [visible, setVisible] = useState(false);
return (
<View style={styles.container}>
<Button title="Показать элементы" onPress={() => setVisible(!visible)} />
{visible && (
<View style={styles.grid}>
<Animated.View entering={FadeIn.duration(600)} style={styles.box}>
<Animated.Text style={styles.label}>FadeIn</Animated.Text>
</Animated.View>
<Animated.View entering={FadeInDown.delay(200).springify()} style={styles.box}>
<Animated.Text style={styles.label}>FadeInDown</Animated.Text>
</Animated.View>
<Animated.View entering={SlideInRight.delay(400).duration(500)} style={styles.box}>
<Animated.Text style={styles.label}>SlideInRight</Animated.Text>
</Animated.View>
<Animated.View entering={BounceIn.delay(600)} style={styles.box}>
<Animated.Text style={styles.label}>BounceIn</Animated.Text>
</Animated.View>
<Animated.View entering={ZoomIn.delay(800).duration(400)} style={styles.box}>
<Animated.Text style={styles.label}>ZoomIn</Animated.Text>
</Animated.View>
<Animated.View entering={FlipInEasyX.delay(1000)} style={styles.box}>
<Animated.Text style={styles.label}>FlipInEasyX</Animated.Text>
</Animated.View>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, justifyContent: 'center' },
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, marginTop: 20 },
box: {
width: 100, height: 80, backgroundColor: '#6C63FF',
borderRadius: 12, justifyContent: 'center', alignItems: 'center',
},
label: { color: '#fff', fontSize: 11, fontWeight: '600' },
});
Анимации исчезновения (exiting) и Layout-переходы
Layout-переходы — это то, что превращает обычный список в приятный для глаз интерфейс. Элементы плавно «разъезжаются», когда один из них удаляется:
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import Animated, { LinearTransition, FadeIn, FadeOut } from 'react-native-reanimated';
export default function LayoutTransitionExample() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const removeItem = (id: number) => {
setItems((prev) => prev.filter((item) => item !== id));
};
return (
<View style={styles.container}>
<Button
title="Добавить элемент"
onPress={() => setItems((prev) => [...prev, Date.now()])}
/>
{items.map((item) => (
<Animated.View
key={item}
entering={FadeIn}
exiting={FadeOut}
layout={LinearTransition.springify().damping(14)}
style={styles.item}
>
<Text style={styles.itemText}>Элемент {item}</Text>
<Button title="✕" onPress={() => removeItem(item)} />
</Animated.View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20 },
item: {
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
backgroundColor: '#F0EFFF', padding: 16, borderRadius: 12, marginTop: 8,
},
itemText: { fontSize: 16, fontWeight: '500' },
});
Интеграция с React Native Gesture Handler
Для интерактивных анимаций, которые управляются жестами пользователя, Reanimated тесно интегрируется с React Native Gesture Handler. Важный момент: в Reanimated 4 хук useAnimatedGestureHandler был полностью удалён. Вместо него используется современный Gesture API из Gesture Handler 2.
Установка Gesture Handler
npx expo install react-native-gesture-handler
Оберните корневой компонент в GestureHandlerRootView:
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Ваше приложение */}
</GestureHandlerRootView>
);
}
Gesture API: основные жесты
Gesture Handler 2 предоставляет объектно-ориентированный API для создания жестов через объект Gesture. Каждый жест создаётся фабричным методом и конфигурируется цепочкой вызовов. Выглядит чисто и читаемо:
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
export default function DraggableBox() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedX = useSharedValue(0);
const savedY = useSharedValue(0);
const scale = useSharedValue(1);
const panGesture = Gesture.Pan()
.onStart(() => {
savedX.value = translateX.value;
savedY.value = translateY.value;
scale.value = withSpring(1.1);
})
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
translateY.value = savedY.value + event.translationY;
})
.onEnd(() => {
scale.value = withSpring(1);
});
const tapGesture = Gesture.Tap()
.onStart(() => {
scale.value = withSpring(0.9);
})
.onEnd(() => {
scale.value = withSpring(1);
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const composedGesture = Gesture.Simultaneous(panGesture, tapGesture);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
}));
return (
<GestureDetector gesture={composedGesture}>
<Animated.View
style={[
{
width: 120, height: 120,
backgroundColor: '#6C63FF', borderRadius: 20,
},
animatedStyle,
]}
/>
</GestureDetector>
);
}
Жест масштабирования (Pinch)
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
export default function PinchableImage() {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const pinchGesture = Gesture.Pinch()
.onUpdate((event) => {
scale.value = savedScale.value * event.scale;
})
.onEnd(() => {
savedScale.value = scale.value;
if (scale.value < 0.5) {
scale.value = withSpring(0.5);
savedScale.value = 0.5;
} else if (scale.value > 3) {
scale.value = withSpring(3);
savedScale.value = 3;
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<GestureDetector gesture={pinchGesture}>
<Animated.Image
source={{ uri: 'https://picsum.photos/300/300' }}
style={[{ width: 300, height: 300, borderRadius: 16 }, animatedStyle]}
/>
</GestureDetector>
);
}
Практический пример: свайп-карточки в стиле Tinder
Ну что ж, давайте соберём все полученные знания в один практический пример — создадим компонент свайп-карточки. Это один из тех элементов, который отлично демонстрирует мощь связки Reanimated + Gesture Handler:
import React, { useState, useCallback } from 'react';
import { View, Text, Image, StyleSheet, Dimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
runOnJS,
Extrapolation,
} from 'react-native-reanimated';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.3;
const ROTATION_ANGLE = 15;
interface CardData {
id: number;
name: string;
age: number;
image: string;
}
const CARDS: CardData[] = [
{ id: 1, name: 'Анна', age: 25, image: 'https://picsum.photos/id/64/400/600' },
{ id: 2, name: 'Мария', age: 28, image: 'https://picsum.photos/id/65/400/600' },
{ id: 3, name: 'Елена', age: 23, image: 'https://picsum.photos/id/66/400/600' },
];
function SwipeCard({ card, onSwipe }: { card: CardData; onSwipe: (dir: 'left' | 'right') => void }) {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const handleSwipe = useCallback(
(direction: 'left' | 'right') => { onSwipe(direction); },
[onSwipe]
);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd((event) => {
if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
const direction = translateX.value > 0 ? 'right' : 'left';
const targetX = translateX.value > 0 ? SCREEN_WIDTH * 1.5 : -SCREEN_WIDTH * 1.5;
translateX.value = withTiming(targetX, { duration: 300 }, () => {
runOnJS(handleSwipe)(direction);
});
translateY.value = withTiming(event.translationY * 2, { duration: 300 });
} else {
translateX.value = withSpring(0, { damping: 15, stiffness: 120 });
translateY.value = withSpring(0, { damping: 15, stiffness: 120 });
}
});
const cardAnimatedStyle = useAnimatedStyle(() => {
const rotation = interpolate(
translateX.value,
[-SCREEN_WIDTH, 0, SCREEN_WIDTH],
[-ROTATION_ANGLE, 0, ROTATION_ANGLE],
Extrapolation.CLAMP
);
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ rotate: `${rotation}deg` },
],
};
});
const likeStyle = useAnimatedStyle(() => ({
opacity: interpolate(translateX.value, [0, SWIPE_THRESHOLD], [0, 1], Extrapolation.CLAMP),
}));
const dislikeStyle = useAnimatedStyle(() => ({
opacity: interpolate(translateX.value, [-SWIPE_THRESHOLD, 0], [1, 0], Extrapolation.CLAMP),
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, cardAnimatedStyle]}>
<Image source={{ uri: card.image }} style={styles.cardImage} />
<Animated.View style={[styles.overlay, styles.likeOverlay, likeStyle]}>
<Text style={[styles.overlayText, { color: '#4CAF50' }]}>НРАВИТСЯ</Text>
</Animated.View>
<Animated.View style={[styles.overlay, styles.dislikeOverlay, dislikeStyle]}>
<Text style={[styles.overlayText, { color: '#F44336' }]}>НЕТ</Text>
</Animated.View>
<View style={styles.cardInfo}>
<Text style={styles.cardName}>{card.name}, {card.age}</Text>
</View>
</Animated.View>
</GestureDetector>
);
}
export default function SwipeCardDeck() {
const [currentIndex, setCurrentIndex] = useState(0);
const handleSwipe = useCallback((direction: 'left' | 'right') => {
setCurrentIndex((prev) => prev + 1);
}, []);
if (currentIndex >= CARDS.length) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>Карточки закончились!</Text>
</View>
);
}
return (
<View style={styles.container}>
<SwipeCard key={CARDS[currentIndex].id} card={CARDS[currentIndex]} onSwipe={handleSwipe} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
card: {
width: SCREEN_WIDTH * 0.85, height: SCREEN_WIDTH * 1.2,
borderRadius: 20, position: 'absolute', backgroundColor: '#fff',
shadowColor: '#000', shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15, shadowRadius: 12, elevation: 8, overflow: 'hidden',
},
cardImage: { width: '100%', height: '100%', borderRadius: 20 },
cardInfo: {
position: 'absolute', bottom: 0, left: 0, right: 0, padding: 20,
backgroundColor: 'rgba(0,0,0,0.3)', borderBottomLeftRadius: 20, borderBottomRightRadius: 20,
},
cardName: { fontSize: 28, fontWeight: 'bold', color: '#fff' },
overlay: { position: 'absolute', top: 40, zIndex: 10, padding: 10, borderWidth: 3, borderRadius: 8 },
likeOverlay: { left: 20, borderColor: '#4CAF50' },
dislikeOverlay: { right: 20, borderColor: '#F44336' },
overlayText: { fontSize: 32, fontWeight: '800' },
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
emptyText: { fontSize: 22, color: '#999' },
});
Этот пример демонстрирует несколько ключевых техник:
- Gesture.Pan() для отслеживания перетаскивания карточки
- interpolate для вычисления угла поворота и прозрачности оверлеев на основе смещения
- withSpring для упругого возврата карточки при недостаточном свайпе
- withTiming для плавного удаления карточки за пределы экрана
- runOnJS для вызова JavaScript-функции из воркалета
- Extrapolation.CLAMP для ограничения интерполированных значений
Оптимизация производительности анимаций
Создание плавных анимаций на 60–120 fps требует понимания того, как Reanimated выполняет код. Давайте разберём основные правила, которые помогут избежать типичных ошибок.
Принцип работы: UI-поток vs JS-поток
Главное преимущество Reanimated — выполнение анимаций на UI-потоке. Shared values живут на UI-потоке, функции внутри useAnimatedStyle и обработчики жестов компилируются в воркалеты. Это значит, что даже если JS-поток загружен тяжёлыми вычислениями (парсинг JSON, сетевые запросы, рендеринг больших списков), анимации продолжают работать плавно.
Ключевые правила оптимизации
import Animated, {
useSharedValue,
useAnimatedReaction,
cancelAnimation,
withRepeat,
withTiming,
runOnJS,
} from 'react-native-reanimated';
// 1. Избегайте вызова setState внутри анимаций без необходимости
// 2. Используйте useAnimatedReaction для реакции на изменения
function OptimizedComponent() {
const progress = useSharedValue(0);
useAnimatedReaction(
() => progress.value,
(currentValue, previousValue) => {
if (currentValue >= 1 && previousValue < 1) {
runOnJS(onAnimationComplete)();
}
}
);
}
// 3. Отменяйте анимации при размонтировании
function CleanupExample() {
const rotation = useSharedValue(0);
React.useEffect(() => {
rotation.value = withRepeat(
withTiming(360, { duration: 3000 }),
-1,
false
);
return () => {
cancelAnimation(rotation);
};
}, []);
}
Советы для достижения 120 fps
- Никогда не вызывайте runOnJS в onUpdate. Используйте его только в onEnd или при критических событиях. Каждый вызов runOnJS — это мост между потоками, и в onUpdate он будет вызываться десятки раз в секунду.
- Используйте interpolate вместо условных выражений. Функция interpolate оптимизирована для UI-потока и работает быстрее, чем цепочки if/else.
- Используйте cancelAnimation при размонтировании компонентов с бесконечными анимациями.
- Предпочитайте withSpring для интерактивных элементов. Пружинные анимации лучше реагируют на прерывание жестом — пользователь может «перехватить» элемент в любой момент.
- Минимизируйте количество shared values. Используйте useDerivedValue для производных значений вместо создания дополнительных shared values.
import { useDerivedValue, interpolate } from 'react-native-reanimated';
const translateX = useSharedValue(0);
const rotation = useDerivedValue(() =>
interpolate(translateX.value, [-200, 0, 200], [-30, 0, 30])
);
const opacity = useDerivedValue(() =>
interpolate(Math.abs(translateX.value), [0, 200], [1, 0.5])
);
Миграция с Reanimated 3 на 4
Если вы обновляете существующий проект, переход с Reanimated 3 на версию 4 содержит несколько критических изменений. Расскажу о каждом.
1. Обязательная New Architecture
Reanimated 4 работает только с New Architecture. Для Expo SDK 55 это не проблема — New Architecture включена по умолчанию. Для bare-проектов на старой архитектуре придётся сначала мигрировать.
2. Вынос воркалетов в отдельный пакет
// Reanimated 3 — СТАРОЕ:
plugins: ['react-native-reanimated/plugin']
// Reanimated 4 — НОВОЕ:
plugins: ['react-native-worklets/plugin']
3. Удаление useAnimatedGestureHandler
Хук useAnimatedGestureHandler был полностью удалён. Вместо него используйте Gesture API из Gesture Handler 2:
// СТАРЫЙ КОД (Reanimated 3):
import { PanGestureHandler } from 'react-native-gesture-handler';
import { useAnimatedGestureHandler } from 'react-native-reanimated';
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, context) => { context.startX = translateX.value; },
onActive: (event, context) => {
translateX.value = context.startX + event.translationX;
},
onEnd: () => { translateX.value = withSpring(0); },
});
// НОВЫЙ КОД (Reanimated 4):
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
const savedX = useSharedValue(0);
const panGesture = Gesture.Pan()
.onStart(() => { savedX.value = translateX.value; })
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
})
.onEnd(() => { translateX.value = withSpring(0); });
4. Удаление useWorkletCallback
// СТАРОЕ:
// const myCallback = useWorkletCallback((value) => { return value * 2; }, []);
// НОВОЕ:
const myCallback = (value: number) => {
'worklet';
return value * 2;
};
5. Чек-лист миграции
- Убедитесь, что проект использует New Architecture
- Установите пакет react-native-worklets
- Обновите babel.config.js: замените плагин на 'react-native-worklets/plugin'
- Замените все использования useAnimatedGestureHandler на Gesture API
- Замените PanGestureHandler и аналоги на GestureDetector
- Удалите использования useWorkletCallback
- Очистите кеш и пересоберите проект
- Протестируйте все анимации и жестовые взаимодействия
Полезные приёмы и паттерны
Анимированные скелетоны загрузки
Скелетоны (skeleton screens) — распространённый паттерн для отображения во время загрузки данных. С CSS-анимациями Reanimated 4 реализация получается на удивление лаконичной:
import React from 'react';
import { View, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';
const shimmerKeyframes = {
'0%': { opacity: 0.3 },
'50%': { opacity: 0.7 },
'100%': { opacity: 0.3 },
};
function SkeletonLine({ width, height = 16, marginBottom = 8 }) {
return (
<Animated.View
style={{
width, height,
backgroundColor: '#E0E0E0',
borderRadius: 4,
marginBottom,
animationName: shimmerKeyframes,
animationDuration: '1.5s',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
}}
/>
);
}
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: 4,
},
});
Анимированная кнопка с тактильной обратной связью
Маленький, но важный паттерн — кнопка, которая «вдавливается» при нажатии. Такая мелочь, а ощущение от приложения совсем другое:
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS,
} from 'react-native-reanimated';
export default function AnimatedButton({ title, onPress, color = '#6C63FF' }) {
const scale = useSharedValue(1);
const tapGesture = Gesture.Tap()
.onBegin(() => {
scale.value = withSpring(0.92, { damping: 15, stiffness: 300 });
})
.onFinalize(() => {
scale.value = withSpring(1, { damping: 10, stiffness: 200 });
})
.onEnd(() => {
runOnJS(onPress)();
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<GestureDetector gesture={tapGesture}>
<Animated.View style={[styles.button, { backgroundColor: color }, animatedStyle]}>
<Text style={styles.buttonText}>{title}</Text>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
button: {
paddingHorizontal: 32, paddingVertical: 14, borderRadius: 12,
alignItems: 'center', shadowColor: '#000',
shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2,
shadowRadius: 8, elevation: 6,
},
buttonText: { color: '#fff', fontSize: 16, fontWeight: '700' },
});
Заключение
Экосистема анимаций React Native в 2026 году достигла впечатляющей зрелости. Reanimated 4.2.1 в связке с Gesture Handler 2 даёт полный набор инструментов для создания анимаций любой сложности — от простых переходов до сложных интерактивных элементов с жестами.
Вот ключевые выводы из этого руководства:
- Используйте Reanimated как основную библиотеку анимаций. Он работает на UI-потоке и обеспечивает стабильные 60–120 fps.
- Применяйте CSS-анимации для простых случаев. Свойства transitionProperty и animationName делают код проще и декларативнее.
- Используйте Gesture API вместо устаревших обработчиков. Gesture.Pan(), Gesture.Pinch(), Gesture.Tap() — современный и гибкий подход.
- Помните о New Architecture. Reanimated 4 требует её обязательно.
- Не забывайте про Babel-плагин. Используйте 'react-native-worklets/plugin', а не устаревший 'react-native-reanimated/plugin'.
- Оптимизируйте: избегайте лишних вызовов runOnJS, используйте useDerivedValue, отменяйте анимации при размонтировании.
- Layout-анимации — мощнейший инструмент для списков и динамических макетов без ручного управления.
Анимации — это та область, где технические навыки сочетаются с чувством вкуса. Начните с простых переходов, постепенно осваивая более сложные техники. Тестируйте на реальных устройствах, экспериментируйте с параметрами пружин и кривыми ускорения. И помните: именно плавность и отзывчивость анимаций отличают продукт, к которому хочется возвращаться, от того, который удаляют после первого запуска.