آموزش انیمیشن در React Native با Reanimated 4: از CSS Animations تا ژست‌ها

راهنمای عملی انیمیشن‌سازی در React Native با Reanimated 4. از CSS Animations و Keyframes تا تعاملات ژست‌محور و بهترین شیوه‌ها برای رسیدن به ۶۰fps — همراه با مثال‌های کد آماده استفاده.

مقدمه

اگه تا حالا یه اپلیکیشن موبایل استفاده کردید که انیمیشن‌هاش واقعاً روان و لذت‌بخش بودن، احتمالاً متوجه شدید که چقدر تأثیر داره. انیمیشن یکی از اون عناصری‌ه که تجربه کاربری رو از «خوب» به «فوق‌العاده» می‌بره — وقتی دکمه‌ای رو لمس می‌کنید و بازخورد بصری نرمی می‌بینید، یا وقتی یه مودال با حرکت طبیعی ظاهر و ناپدید می‌شه.

راستش رو بخواید، من خودم وقتی اولین بار Reanimated 4 رو امتحان کردم، واقعاً جا خوردم.

در دنیای React Native، کتابخانه Reanimated از تیم Software Mansion سال‌هاست که استاندارد طلایی ساخت انیمیشن‌های پرفورمنت بالاست. ولی نسخه ۴ یه جهش بزرگ محسوب می‌شه: حالا می‌تونید انیمیشن‌های CSS رو مستقیماً در React Native بنویسید! بله، همون transition و @keyframes آشنای دنیای وب، الان روی موبایل هم کار می‌کنن. و نکته مهم اینه که همه چیز روی ترد UI نیتیو اجرا می‌شه تا ۶۰ فریم بر ثانیه تضمین بشه.

توی این راهنما، از مفاهیم پایه تا الگوهای پیشرفته انیمیشن‌سازی رو با هم مرور می‌کنیم. فرقی نمی‌کنه تازه‌کار باشید یا از نسخه ۳ دارید مهاجرت می‌کنید — همه چیزی که لازم دارید اینجاست.

Reanimated 4 چیست و چه تغییراتی داره؟

React Native Reanimated یه کتابخانه انیمیشن‌ه که با استفاده از JSI (JavaScript Interface) انیمیشن‌ها رو مستقیماً روی ترد UI اجرا می‌کنه. برخلاف API پیش‌فرض Animated در React Native که محاسبات رو روی ترد جاوااسکریپت انجام می‌ده، Reanimated با اجرای کد روی ترد نیتیو از افت فریم و لگ جلوگیری می‌کنه.

خب حالا ببینیم نسخه ۴ چه چیزایی عوض کرده.

تغییرات کلیدی نسخه ۴ نسبت به نسخه ۳

  • پشتیبانی از انیمیشن‌های CSS: بزرگ‌ترین ویژگی جدید! حالا می‌تونید از transitionProperty، transitionDuration، animationName و Keyframes مثل دنیای وب استفاده کنید.
  • فقط معماری جدید (New Architecture): Reanimated 4 منحصراً با معماری Fabric کار می‌کنه. اگه هنوز از معماری قدیمی (Paper) استفاده می‌کنید، باید روی نسخه ۳ بمونید یا اول به React Native 0.76+ مهاجرت کنید.
  • جداسازی Worklets: پیاده‌سازی Workletها به پکیج مستقل react-native-worklets منتقل شده. این رویکرد ماژولار به سایر کتابخانه‌ها هم اجازه می‌ده از Workletها بهره ببرن.
  • حذف APIهای منسوخ: useAnimatedGestureHandler کاملاً حذف شده و باید از Gesture API نسخه ۲ استفاده کنید. همچنین useScrollViewOffset به useScrollOffset تغییر نام داده.
  • سازگاری با عقب: خبر خوب اینه که انیمیشن‌های موجودتون با Shared Values و useAnimatedStyle بدون تغییر کار می‌کنن. نفس راحت بکشید!

جدول مقایسه Reanimated 3 و 4

ویژگیReanimated 3Reanimated 4
معماری پشتیبانی‌شدهقدیمی + جدیدفقط جدید (Fabric)
انیمیشن‌های CSS
useAnimatedGestureHandlerمنسوخحذف شده
پکیج Workletsداخلیreact-native-worklets
پلاگین Babelreanimated/pluginworklets/plugin
پشتیبانی از موتور V8حذف شده

نصب و راه‌اندازی Reanimated 4

قبل از هر چیز، مطمئن بشید که پروژه‌تون از React Native 0.76 یا بالاتر با معماری جدید (Fabric) استفاده می‌کنه. بعد از اون، نصب واقعاً ساده‌ست.

نصب در پروژه 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

فایل babel.config.js رو باز کنید و پلاگین Worklets رو اضافه کنید. یه نکته مهم: این پلاگین باید آخرین آیتم توی لیست پلاگین‌ها باشه (این رو فراموش نکنید وگرنه ممکنه با خطاهای عجیبی مواجه بشید):

// babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    // سایر پلاگین‌ها...
    'react-native-worklets/plugin', // باید آخرین باشه
  ],
};

اگه از Expo استفاده می‌کنید:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: ['react-native-worklets/plugin'],
  };
};

تایید نصب

برای اطمینان از درست بودن نصب، یه کامپوننت ساده بسازید و امتحان کنید:

import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { Button, View } from 'react-native';

export default function TestAnimation() {
  const offset = useSharedValue(0);

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

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Animated.View
        style={[
          { width: 100, height: 100, backgroundColor: '#6C63FF', borderRadius: 16 },
          animatedStyle,
        ]}
      />
      <Button
        title="حرکت"
        onPress={() => {
          offset.value = withSpring(offset.value === 0 ? 150 : 0);
        }}
      />
    </View>
  );
}

اگه با زدن دکمه، مربع با انیمیشن فنری حرکت کرد، تبریک می‌گم! همه چیز درسته و آماده‌اید.

مفاهیم پایه: Shared Values و Animated Styles

قبل از اینکه بریم سراغ چیزای جذاب‌تر، باید دو مفهوم بنیادی رو خوب درک کنید. این دوتا ستون اصلی Reanimated هستن.

Shared Values (مقادیر مشترک)

Shared Value یه نوع متغیر خاصه که مقدارش بین ترد جاوااسکریپت و ترد UI به اشتراک گذاشته می‌شه. برخلاف State معمولی React، تغییر مقدار Shared Value باعث رندر مجدد کامپوننت نمی‌شه. در عوض، مستقیماً استایل‌های انیمیشنی رو به‌روزرسانی می‌کنه.

این خیلی مهمه — چون رندر مجدد دشمن پرفورمنسه.

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;
  opacity.value = 0.5;

  // با انیمیشن تغییر بده
  translateX.value = withSpring(200);
  opacity.value = withTiming(0, { duration: 500 });
}

Animated Styles (استایل‌های انیمیشنی)

هوک useAnimatedStyle یه Worklet هست که هر وقت Shared Valueهای داخلش تغییر کنن، روی ترد UI اجرا می‌شه و استایل جدید رو بدون دخالت ترد JS محاسبه می‌کنه:

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

function AnimatedCard() {
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);
  const backgroundColor = useSharedValue('#6C63FF');

  const cardStyle = useAnimatedStyle(() => ({
    transform: [
      { scale: scale.value },
      { rotateZ: `${rotation.value}deg` },
    ],
    backgroundColor: backgroundColor.value,
  }));

  const handlePress = () => {
    scale.value = withSpring(scale.value === 1 ? 1.2 : 1);
    rotation.value = withTiming(rotation.value + 360, {
      duration: 800,
      easing: Easing.bezier(0.25, 0.1, 0.25, 1),
    });
  };

  return (
    <Animated.View style={[styles.card, cardStyle]}>
      <Text onPress={handlePress}>لمس کنید</Text>
    </Animated.View>
  );
}

Worklets چیست؟

Worklet یه تابع جاوااسکریپت خاصه که با دایرکتیو 'worklet' علامت‌گذاری می‌شه و مستقیماً روی ترد UI اجرا می‌شه. صادقانه بگم، این همون رمز اصلی عملکرد بالای Reanimated هست. منطق انیمیشن شما مستقل از ترد JS کار می‌کنه و حتی اگه ترد JS سرش شلوغ باشه، انیمیشن‌ها همچنان روان می‌مونن:

import { runOnUI } from 'react-native-worklets';

// این تابع روی ترد UI اجرا می‌شه
function calculatePosition(x, y) {
  'worklet';
  return Math.sqrt(x * x + y * y);
}

انیمیشن‌های CSS: قابلیت انقلابی Reanimated 4

خب، بریم سراغ اصل ماجرا. بزرگ‌ترین نوآوری Reanimated 4 اینه که حالا می‌تونید از سینتکس آشنای CSS برای ساخت انیمیشن استفاده کنید. اگه تجربه توسعه وب دارید، این قابلیت براتون خیلی طبیعی خواهد بود — تقریباً مثل خونه‌تونه!

CSS Transitions (انتقال‌ها)

Transitionها ساده‌ترین نوع انیمیشن CSS هستن. فقط کافیه مشخص کنید کدوم ویژگی‌ها باید انیمیشن داشته باشن و وقتی مقادیر از طریق State عوض بشن، Reanimated خودش انتقال روان رو مدیریت می‌کنه. دیگه نیازی نیست Shared Value تعریف کنید.

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

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

  return (
    <View style={{ padding: 20 }}>
      <Pressable onPress={() => setExpanded(!expanded)}>
        <Animated.View
          style={{
            width: expanded ? 300 : 150,
            height: expanded ? 200 : 100,
            backgroundColor: expanded ? '#FF6B6B' : '#6C63FF',
            borderRadius: 16,
            // تعریف Transition — مثل CSS!
            transitionProperty: 'width, height, backgroundColor',
            transitionDuration: 400,
            transitionTimingFunction: 'ease-in-out',
            justifyContent: 'center',
            alignItems: 'center',
          }}
        >
          <Animated.Text
            style={{
              color: 'white',
              fontSize: expanded ? 18 : 14,
              transitionProperty: 'fontSize',
              transitionDuration: 300,
            }}
          >
            {expanded ? 'بزرگ شدم!' : 'لمس کن'}
          </Animated.Text>
        </Animated.View>
      </Pressable>
    </View>
  );
}

نکته مهم: انیمیشن‌های CSS به صورت اعلانی (Declarative) کار می‌کنن. یعنی شما فقط مقادیر جدید رو از طریق State ست می‌کنید و Reanimated خودش انتقال بین مقادیر قدیم و جدید رو با انیمیشن انجام می‌ده. به همین سادگی.

CSS Keyframes (کلیدفریم‌ها)

برای انیمیشن‌های پیچیده‌تر که چند مرحله دارن، Keyframes بهترین انتخابه. ساختارش دقیقاً شبیه @keyframes در CSS هست، پس اگه با وب کار کرده باشید خیلی آشنا به نظر می‌رسه:

import Animated from 'react-native-reanimated';
import type { CSSAnimationKeyframes } from 'react-native-reanimated';

// تعریف Keyframe — مثل @keyframes در CSS
const pulse: CSSAnimationKeyframes = {
  from: {
    transform: [{ scale: 1 }],
    opacity: 1,
  },
  '50%': {
    transform: [{ scale: 1.15 }],
    opacity: 0.7,
  },
  to: {
    transform: [{ scale: 1 }],
    opacity: 1,
  },
};

const shimmer: CSSAnimationKeyframes = {
  from: {
    backgroundColor: '#e0e0e0',
  },
  '50%': {
    backgroundColor: '#f5f5f5',
  },
  to: {
    backgroundColor: '#e0e0e0',
  },
};

function PulsingButton() {
  return (
    <Animated.View
      style={{
        width: 80,
        height: 80,
        borderRadius: 40,
        backgroundColor: '#FF6B6B',
        // اعمال انیمیشن Keyframe
        animationName: pulse,
        animationDuration: '2s',
        animationIterationCount: 'infinite',
        animationTimingFunction: 'ease-in-out',
      }}
    />
  );
}

function SkeletonLoader() {
  return (
    <Animated.View
      style={{
        width: '100%',
        height: 20,
        borderRadius: 4,
        animationName: shimmer,
        animationDuration: '1.5s',
        animationIterationCount: 'infinite',
      }}
    />
  );
}

اجرای چند انیمیشن همزمان

یه قابلیت باحال دیگه اینه که می‌تونید چندین انیمیشن Keyframe رو همزمان روی یه المان اجرا کنید:

const fadeInOut: CSSAnimationKeyframes = {
  from: { opacity: 0 },
  to: { opacity: 1 },
};

const slideUp: CSSAnimationKeyframes = {
  from: { transform: [{ translateY: 50 }] },
  to: { transform: [{ translateY: 0 }] },
};

function AnimatedEntry() {
  return (
    <Animated.View
      style={{
        animationName: [fadeInOut, slideUp],
        animationDuration: ['0.5s', '0.8s'],
        animationFillMode: 'forwards',
      }}
    >
      <Text>ورود با انیمیشن</Text>
    </Animated.View>
  );
}

ویژگی‌های قابل تنظیم انیمیشن CSS

لیست کاملی از ویژگی‌هایی که می‌تونید تنظیم کنید:

  • animationName — آبجکت Keyframe یا آرایه‌ای از Keyframeها
  • animationDuration — مدت زمان انیمیشن (مثلاً '1s' یا 500)
  • animationDelay — تاخیر قبل از شروع انیمیشن
  • animationIterationCount — تعداد تکرار ('infinite' برای بی‌نهایت)
  • animationDirection — جهت انیمیشن ('normal'، 'reverse'، 'alternate')
  • animationTimingFunction — تابع زمان‌بندی ('ease'، 'linear'، 'ease-in-out')
  • animationFillMode — حالت پایانی ('forwards'، 'backwards'، 'both')
  • animationPlayState — کنترل پخش/توقف ('running'، 'paused')

انیمیشن‌های Worklet-Based: کنترل فریم‌به‌فریم

انیمیشن‌های CSS برای اکثر سناریوها عالی هستن. ولی وقتی به کنترل دقیق فریم‌به‌فریم یا انیمیشن‌های مبتنی بر ژست نیاز دارید، سیستم Worklet-Based همچنان بهترین دوست شماست.

withTiming — انیمیشن زمان‌بندی‌شده

ساده‌ترین نوع انیمیشن. مقدار رو طی یه مدت زمان مشخص از A به B می‌بره:

import { withTiming, Easing } from 'react-native-reanimated';

// انیمیشن ساده - 300 میلی‌ثانیه پیش‌فرض
opacity.value = withTiming(0);

// با تنظیمات سفارشی
scale.value = withTiming(1.5, {
  duration: 600,
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});

// با callback پس از اتمام
translateY.value = withTiming(100, { duration: 500 }, (finished) => {
  if (finished) {
    // انیمیشن کامل شد
    runOnJS(onAnimationComplete)();
  }
});

withSpring — انیمیشن فنری

انیمیشن فنری حس طبیعی‌تری داره و شخصاً من توی بیشتر پروژه‌هام ازش استفاده می‌کنم. به جای مدت زمان ثابت، از فیزیک فنر استفاده می‌کنه و نتیجه‌ش خیلی واقعی‌تر به نظر می‌رسه:

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

// فنر پیش‌فرض
translateX.value = withSpring(200);

// فنر سفارشی - هر پارامتر حس متفاوتی ایجاد می‌کنه
scale.value = withSpring(1.2, {
  damping: 15,        // میرایی — مقدار کمتر = نوسان بیشتر
  stiffness: 150,     // سختی — مقدار بیشتر = سریع‌تر
  mass: 1,            // جرم — مقدار بیشتر = کندتر و سنگین‌تر
  overshootClamping: false,  // اگه true باشه، از مقدار هدف رد نمی‌شه
});

// فنر نرم برای انیمیشن‌های ظریف (مثل تغییر اندازه)
height.value = withSpring(300, {
  damping: 20,
  stiffness: 90,
});

// فنر تند برای بازخورد سریع (مثل دکمه‌ها)
buttonScale.value = withSpring(0.95, {
  damping: 10,
  stiffness: 400,
});

ترکیب انیمیشن‌ها: متوالی و موازی

اینجاست که کار واقعاً جالب می‌شه. می‌تونید انیمیشن‌ها رو پشت سر هم زنجیره کنید:

import {
  withSequence,
  withDelay,
  withTiming,
  withSpring,
} from 'react-native-reanimated';

// انیمیشن متوالی — هر مرحله بعد از قبلی اجرا می‌شه
scale.value = withSequence(
  withTiming(1.3, { duration: 200 }),   // بزرگ شو
  withTiming(0.9, { duration: 150 }),   // کوچک شو
  withSpring(1)                          // به حالت عادی برگرد
);

// انیمیشن با تاخیر
opacity.value = withDelay(
  500,                                   // 500ms تاخیر
  withTiming(1, { duration: 300 })       // بعد شروع شو
);

// انیمیشن تکرارشونده
rotation.value = withRepeat(
  withTiming(360, { duration: 1000 }),
  -1,     // -1 یعنی بی‌نهایت
  false   // reverse نباشه
);

تعاملات ژست‌محور با Gesture Handler

یکی از قدرتمندترین کاربردهای Reanimated، ترکیبش با react-native-gesture-handler برای ساخت تعاملات لمسی روانه. اون لحظه‌ای که کاربر المانی رو با انگشت می‌کشه و انیمیشن بدون هیچ تاخیری دنبالش حرکت می‌کنه — همون حسی‌ه که با این ترکیب به دست میاد.

توی Reanimated 4 باید از Gesture API نسخه ۲ استفاده کنید (چون useAnimatedGestureHandler حذف شده).

نصب Gesture Handler

npx expo install react-native-gesture-handler
# یا
npm install react-native-gesture-handler

کشیدن و رها کردن (Drag and Drop)

بذارید با یه مثال کلاسیک شروع کنیم — یه کارت قابل کشیدن:

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

function DraggableCard() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const scale = useSharedValue(1);

  const pan = Gesture.Pan()
    .onStart(() => {
      // شروع کشیدن — کمی بزرگ شو
      scale.value = withSpring(1.1);
    })
    .onChange((event) => {
      // حین کشیدن — دنبال انگشت حرکت کن
      translateX.value += event.changeX;
      translateY.value += event.changeY;
    })
    .onEnd(() => {
      // رها کردن — به جای اول برگرد
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
      scale.value = withSpring(1);
    });

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

  return (
    <GestureDetector gesture={pan}>
      <Animated.View
        style={[
          {
            width: 150,
            height: 150,
            backgroundColor: '#6C63FF',
            borderRadius: 20,
          },
          animatedStyle,
        ]}
      />
    </GestureDetector>
  );
}

Swipe to Delete (کشیدن برای حذف)

این الگو رو حتماً توی اپ‌های مختلف دیدید. پیاده‌سازیش با Reanimated اینطوری می‌شه:

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
  runOnJS,
  interpolateColor,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { Dimensions, Text, View } from 'react-native';

const SCREEN_WIDTH = Dimensions.get('window').width;
const SWIPE_THRESHOLD = -SCREEN_WIDTH * 0.3;

function SwipeableItem({ title, onDelete }) {
  const translateX = useSharedValue(0);

  const pan = Gesture.Pan()
    .activeOffsetX([-10, 10])
    .onChange((event) => {
      // فقط به سمت چپ اجازه بده
      if (translateX.value + event.changeX < 0) {
        translateX.value += event.changeX;
      }
    })
    .onEnd(() => {
      if (translateX.value < SWIPE_THRESHOLD) {
        // به اندازه کافی کشیده شده — حذف کن
        translateX.value = withTiming(-SCREEN_WIDTH, {}, () => {
          runOnJS(onDelete)();
        });
      } else {
        // برگرد سر جات
        translateX.value = withSpring(0);
      }
    });

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

  const bgStyle = useAnimatedStyle(() => ({
    backgroundColor: interpolateColor(
      translateX.value,
      [0, SWIPE_THRESHOLD],
      ['#ffffff', '#FF4444']
    ),
  }));

  return (
    <Animated.View style={[{ height: 80 }, bgStyle]}>
      <GestureDetector gesture={pan}>
        <Animated.View style={[styles.item, itemStyle]}>
          <Text>{title}</Text>
        </Animated.View>
      </GestureDetector>
    </Animated.View>
  );
}

Pinch to Zoom (نزدیک‌نمایی با دو انگشت)

برای گالری تصاویر یا هر جایی که نیاز به زوم دارید:

function PinchableImage({ source }) {
  const scale = useSharedValue(1);
  const savedScale = useSharedValue(1);

  const pinch = Gesture.Pinch()
    .onUpdate((event) => {
      scale.value = savedScale.value * event.scale;
    })
    .onEnd(() => {
      // حداقل 1 و حداکثر 4 برابر
      const clampedScale = Math.min(Math.max(scale.value, 1), 4);
      scale.value = withSpring(clampedScale);
      savedScale.value = clampedScale;
    });

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

  return (
    <GestureDetector gesture={pinch}>
      <Animated.Image source={source} style={[styles.image, imageStyle]} />
    </GestureDetector>
  );
}

Layout Animations: انیمیشن‌های ورود، خروج و تغییر چیدمان

یکی از جذاب‌ترین قابلیت‌های Reanimated اینه که می‌تونید انیمیشن‌های خودکار برای ورود و خروج المان‌ها تعریف کنید. یعنی وقتی یه آیتم به لیست اضافه یا ازش حذف می‌شه، خودش با انیمیشن ظاهر و محو می‌شه. خیلی تمیز و حرفه‌ای.

انیمیشن‌های ورود و خروج

import Animated, {
  FadeIn,
  FadeOut,
  SlideInRight,
  SlideOutLeft,
  BounceIn,
  ZoomIn,
  Layout,
} from 'react-native-reanimated';

function NotificationList({ notifications }) {
  return (
    <View>
      {notifications.map((item) => (
        <Animated.View
          key={item.id}
          entering={SlideInRight.duration(400).springify()}
          exiting={SlideOutLeft.duration(300)}
          layout={Layout.springify()}
          style={styles.notification}
        >
          <Text>{item.message}</Text>
        </Animated.View>
      ))}
    </View>
  );
}

انیمیشن‌های ورود از پیش‌تعریف‌شده

Reanimated مجموعه خوبی از انیمیشن‌های آماده داره:

  • Fade: FadeIn، FadeInUp، FadeInDown، FadeInLeft، FadeInRight
  • Slide: SlideInRight، SlideInLeft، SlideInUp، SlideInDown
  • Zoom: ZoomIn، ZoomInRotate، ZoomInEasyUp
  • Bounce: BounceIn، BounceInUp، BounceInDown
  • Stretch: StretchInX، StretchInY
  • Roll: RollInLeft، RollInRight

همه اینا قابل سفارشی‌سازی هستن:

// تنظیم مدت زمان
FadeIn.duration(500)

// تنظیم تاخیر
SlideInRight.delay(200)

// استفاده از فیزیک فنری
BounceIn.springify().damping(15)

// ترکیب چند تنظیم
ZoomIn
  .duration(600)
  .delay(100)
  .springify()
  .damping(12)
  .stiffness(100)

Keyframe سفارشی برای ورود

اگه انیمیشن‌های آماده کافی نبودن، می‌تونید Keyframe سفارشی خودتون رو بنویسید:

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

const customEntrance = new Keyframe({
  0: {
    opacity: 0,
    transform: [{ scale: 0.3 }, { rotate: '-45deg' }],
  },
  50: {
    opacity: 0.7,
    transform: [{ scale: 1.1 }, { rotate: '10deg' }],
  },
  100: {
    opacity: 1,
    transform: [{ scale: 1 }, { rotate: '0deg' }],
  },
});

function AnimatedItem() {
  return (
    <Animated.View entering={customEntrance.duration(800)}>
      <Text>ورود سفارشی</Text>
    </Animated.View>
  );
}

پکیج آماده: react-native-css-animations

اگه نمی‌خواید خودتون Keyframe بنویسید (که کاملاً قابل درکه!)، پکیج react-native-css-animations از تیم Software Mansion یه مجموعه خوب از انیمیشن‌های آماده ارائه می‌ده:

npm install react-native-css-animations
import { spin, pulse, bounce, ping, shimmer } from 'react-native-css-animations';
import Animated from 'react-native-reanimated';

// اسپینر چرخشی
function LoadingSpinner() {
  return (
    <Animated.View style={[styles.spinner, spin]} />
  );
}

// دکمه ضربان‌دار
function PulsingDot() {
  return (
    <Animated.View style={[styles.dot, pulse]} />
  );
}

// افکت Shimmer برای Skeleton Loading
function SkeletonLine() {
  return (
    <Animated.View style={[styles.skeleton, shimmer]} />
  );
}

کِی از CSS Animations و کِی از Worklets استفاده کنیم؟

حالا که دو سیستم انیمیشن قدرتمند دارید، سوال اصلی اینه: کدوم رو کی استفاده کنیم؟ جدول زیر کمکتون می‌کنه:

سناریوCSS AnimationsWorklets
نمایش/مخفی کردن المان‌ها✅ ایده‌آلممکن ولی پیچیده‌تر
تغییر رنگ و اندازه با State✅ ایده‌آلممکن
انیمیشن‌های تکرارشونده (لودر)✅ عالیممکن
تعامل ژست‌محور (کشیدن)✅ ضروری
انیمیشن مبتنی بر اسکرول✅ ضروری
فیزیک پیچیده (فنر، جاذبه)محدود✅ ایده‌آل
کنترل لحظه‌ای و دقیق✅ ضروری

یه قانون ساده: اگه انیمیشن شما وابسته به State هست و نیازی به کنترل فریم‌به‌فریم نداره، از CSS Animations استفاده کنید. ولی اگه به تعامل لمسی، اسکرول یا کنترل دقیق نیاز دارید، Worklets رو انتخاب کنید.

مهاجرت از Reanimated 3 به 4

اگه پروژه‌ای دارید که از Reanimated 3 استفاده می‌کنه، خبر خوب اینه که مهاجرت به نسخه ۴ اونقدرا هم سخت نیست. بذارید مهم‌ترین تغییرات رو مرحله‌به‌مرحله مرور کنیم.

۱. نصب پکیج‌های جدید

npm install react-native-reanimated@latest react-native-worklets

۲. به‌روزرسانی پلاگین Babel

// قبل (Reanimated 3)
plugins: ['react-native-reanimated/plugin']

// بعد (Reanimated 4)
plugins: ['react-native-worklets/plugin']

۳. جایگزینی useAnimatedGestureHandler

این احتمالاً بزرگ‌ترین تغییری‌ه که باید انجام بدید:

// قبل (Reanimated 3) — حذف شده!
const gestureHandler = useAnimatedGestureHandler({
  onStart: (_, ctx) => { ctx.startX = translateX.value; },
  onActive: (event, ctx) => { translateX.value = ctx.startX + event.translationX; },
  onEnd: () => { translateX.value = withSpring(0); },
});

// بعد (Reanimated 4) — از Gesture API استفاده کنید
const pan = Gesture.Pan()
  .onStart(() => { startX.value = translateX.value; })
  .onChange((event) => { translateX.value = startX.value + event.translationX; })
  .onEnd(() => { translateX.value = withSpring(0); });

۴. تغییر نام useScrollViewOffset

// قبل
const offset = useScrollViewOffset(scrollRef);

// بعد
const offset = useScrollOffset(scrollRef);

۵. به‌روزرسانی importها از react-native-worklets

// این importها هنوز از reanimated کار می‌کنن ولی منسوخ شدن
// بهتره مستقیم از worklets وارد کنید:
import { runOnUI, runOnJS } from 'react-native-worklets';

بهترین شیوه‌ها برای انیمیشن‌های ۶۰fps

ساخت انیمیشن خوب فقط نوشتن کد نیست. باید مطمئن بشید که روی دستگاه‌های واقعی هم روان اجرا می‌شه. من چندین بار توی پروژه‌ها دیدم که انیمیشنی توی شبیه‌ساز عالی کار می‌کنه ولی روی گوشی واقعی (مخصوصاً اندرویدهای میان‌رده) لگ داره. پس این نکات رو جدی بگیرید.

۱. همیشه در بیلد Release تست کنید

حالت Development به خاطر ابزارهای دیباگ خیلی کندتره. هرگز عملکرد انیمیشن رو در حالت Debug قضاوت نکنید — تفاوتش واقعاً چشمگیره.

۲. از transform و opacity برای انیمیشن استفاده کنید

این ویژگی‌ها توسط GPU پردازش می‌شن و از همه سریع‌ترن. انیمیشن width، height یا margin باعث محاسبه مجدد Layout می‌شه که هزینه بیشتری داره. تا جایی که ممکنه به جای تغییر اندازه واقعی، از scale استفاده کنید.

۳. console.log رو از کد انیمیشن حذف کنید

فراخوانی‌های console.* در Workletها باعث ارتباط بین تردها و افت عملکرد می‌شن. این یکی از اون اشتباهات رایجیه که خیلی‌ها بهش توجه نمی‌کنن.

۴. از cancelAnimation استفاده کنید

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

// وقتی کامپوننت Unmount می‌شه، انیمیشن رو لغو کنید
useEffect(() => {
  return () => {
    cancelAnimation(translateX);
    cancelAnimation(opacity);
  };
}, []);

۵. برای لیست‌ها از scrollEventThrottle استفاده کنید

// برای انیمیشن‌های مبتنی بر اسکرول، این مقدار رو 16 بذارید
<Animated.ScrollView scrollEventThrottle={16}>
  {/* محتوا */}
</Animated.ScrollView>

۶. از Perf Monitor استفاده کنید

در حالت Development، از منوی Dev گزینه Perf Monitor رو فعال کنید تا فریم‌ریت JS و UI رو به صورت زنده ببینید. هدفتون باید ثابت نگه داشتن فریم‌ریت UI روی ۶۰fps باشه — اگه افت کرد، وقتشه که کدتون رو بررسی کنید.

سوالات متداول

آیا برای استفاده از Reanimated 4 حتماً باید به معماری جدید React Native مهاجرت کنم؟

بله، متأسفانه گزینه دیگه‌ای نیست. Reanimated 4 فقط با معماری جدید (Fabric) کار می‌کنه که از React Native 0.76 به بعد به‌صورت پیش‌فرض فعاله. اگه هنوز از معماری قدیمی استفاده می‌کنید، می‌تونید فعلاً روی Reanimated 3 بمونید (که همچنان نگهداری می‌شه) یا اول پروژه‌تون رو به New Architecture مهاجرت بدید.

تفاوت انیمیشن‌های CSS در Reanimated با Animated API پیش‌فرض React Native چیه؟

API پیش‌فرض Animated محاسبات رو روی ترد جاوااسکریپت انجام می‌ده (مگه اینکه useNativeDriver: true بذارید که اون هم محدودیت‌های خاص خودش رو داره). ولی انیمیشن‌های CSS در Reanimated 4 مستقیماً روی ترد UI نیتیو اجرا می‌شن و از تمام ویژگی‌های استایل پشتیبانی می‌کنن — از transform و opacity گرفته تا backgroundColor و width/height.

آیا می‌شه از Reanimated 4 همراه با react-native-gesture-handler استفاده کرد؟

صد درصد بله! Reanimated 4 با Gesture Handler نسخه ۲ به‌صورت عمیق یکپارچه شده. تنها تغییر مهم اینه که useAnimatedGestureHandler حذف شده و باید از Gesture API جدید (Gesture.Pan()، Gesture.Pinch() و غیره) استفاده کنید که هم API بهتری داره و هم عملکرد بالاتری.

چطور مطمئن بشم انیمیشن‌هام واقعاً ۶۰fps هستن؟

سه روش اصلی وجود داره: اول، Perf Monitor رو از منوی Dev فعال کنید تا فریم‌ریت زنده رو ببینید. دوم، حتماً در بیلد Release تست کنید چون حالت Debug خیلی کندتره و نتایجش قابل اتکا نیست. سوم، از ابزارهایی مثل React Native DevTools یا Radon IDE برای شناسایی گلوگاه‌ها استفاده کنید.

آیا مهاجرت از Reanimated 3 به 4 سخته؟

بستگی داره! اگه پروژه‌تون از useAnimatedGestureHandler استفاده نمی‌کنه، مهاجرت نسبتاً ساده‌ست — عمدتاً نصب react-native-worklets و تغییر پلاگین Babel کافیه. ولی اگه از useAnimatedGestureHandler استفاده می‌کنید، باید اون بخش‌ها رو به Gesture API جدید بازنویسی کنید. پیشنهادم اینه که مهاجرت رو مرحله‌به‌مرحله انجام بدید و از بخش‌های کوچک شروع کنید.

درباره نویسنده Editorial Team

Our team of expert writers and editors.