6.7 KiB
6.7 KiB
Animations Reference
Reanimated 3 animations, gestures, and transitions for Expo/React Native.
Core Rules
- Only animate
transformandopacity— GPU-composited, no layout recalculation - Use
useDerivedValuefor computed animated values, not inline JS expressions - Use
Gesture.Tapinstead ofPressableinsideGestureDetector - All Reanimated callbacks run as worklets on the UI thread — no async/await
Setup
npx expo install react-native-reanimated react-native-gesture-handler
// babel.config.js
module.exports = { presets: ["babel-preset-expo"], plugins: ["react-native-reanimated/plugin"] };
// app/_layout.tsx — wrap root in GestureHandlerRootView
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function RootLayout() {
return <GestureHandlerRootView style={{ flex: 1 }}><Stack /></GestureHandlerRootView>;
}
Entering / Exiting Animations
import Animated, {
FadeIn, FadeOut,
SlideInRight, SlideOutLeft,
ZoomIn, ZoomOut,
BounceIn,
} from "react-native-reanimated";
// Basic
<Animated.View entering={FadeIn} exiting={FadeOut}>
<Text>Content</Text>
</Animated.View>
// With options
<Animated.View
entering={FadeIn.duration(300).delay(100)}
exiting={SlideOutLeft.duration(200)}
/>
// Spring-based
<Animated.View entering={ZoomIn.springify().damping(15)} />
Built-in Presets
| Category | Entering | Exiting |
|---|---|---|
| Fade | FadeIn, FadeInUp, FadeInDown, FadeInLeft, FadeInRight |
FadeOut* |
| Slide | SlideInUp, SlideInDown, SlideInLeft, SlideInRight |
SlideOut* |
| Zoom | ZoomIn, ZoomInUp, ZoomInDown |
ZoomOut* |
| Bounce | BounceIn, BounceInUp, BounceInDown |
BounceOut* |
| Flip | FlipInXUp, FlipInYLeft |
FlipOut* |
| Roll | RollInLeft, RollInRight |
RollOut* |
| Stretch | StretchInX, StretchInY |
StretchOut* |
| Pinwheel | PinwheelIn |
PinwheelOut |
| Rotate | RotateInDownLeft |
RotateOut* |
| LightSpeed | LightSpeedInLeft |
LightSpeedOut* |
Shared Values & useAnimatedStyle
import {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
withRepeat,
withSequence,
Easing,
} from "react-native-reanimated";
const offset = useSharedValue(0);
const opacity = useSharedValue(1);
const animStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
opacity: opacity.value,
}));
// Animate
offset.value = withSpring(100);
opacity.value = withTiming(0, { duration: 300, easing: Easing.out(Easing.quad) });
// Repeat
opacity.value = withRepeat(withTiming(0.3, { duration: 800 }), -1, true);
// Sequence
offset.value = withSequence(
withTiming(-10, { duration: 100 }),
withTiming(10, { duration: 100 }),
withSpring(0),
);
<Animated.View style={animStyle} />
useDerivedValue
import { useDerivedValue } from "react-native-reanimated";
const progress = useSharedValue(0); // 0–1
const rotation = useDerivedValue(() => `${progress.value * 360}deg`);
const scale = useDerivedValue(() => 0.5 + progress.value * 0.5);
const animStyle = useAnimatedStyle(() => ({
transform: [{ rotate: rotation.value }, { scale: scale.value }],
}));
Layout Animations
import { Layout, LinearTransition, CurvedTransition } from "react-native-reanimated";
// Item reorder/add/remove animation
<Animated.View layout={LinearTransition}>
{/* Content that changes size/position */}
</Animated.View>
// Spring layout transition
<Animated.View layout={LinearTransition.springify()}>
Gestures
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
// Pan gesture
const offsetX = useSharedValue(0);
const offsetY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onUpdate((e) => {
offsetX.value = e.translationX;
offsetY.value = e.translationY;
})
.onEnd(() => {
offsetX.value = withSpring(0);
offsetY.value = withSpring(0);
});
const animStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offsetX.value }, { translateY: offsetY.value }],
}));
<GestureDetector gesture={panGesture}>
<Animated.View style={animStyle} />
</GestureDetector>
// Tap gesture (use instead of Pressable inside GestureDetector)
const tapGesture = Gesture.Tap()
.numberOfTaps(1)
.onEnd(() => {
scale.value = withSequence(withTiming(0.95), withSpring(1));
});
// Pinch gesture
const baseScale = useSharedValue(1);
const savedScale = useSharedValue(1);
const pinchGesture = Gesture.Pinch()
.onUpdate((e) => { baseScale.value = savedScale.value * e.scale; })
.onEnd(() => { savedScale.value = baseScale.value; });
// Composed gestures
const composed = Gesture.Simultaneous(panGesture, pinchGesture);
const exclusive = Gesture.Exclusive(tapGesture, panGesture);
Scroll-Driven Animations
import Animated, {
useAnimatedScrollHandler,
useSharedValue,
interpolate,
Extrapolation,
} from "react-native-reanimated";
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler((e) => {
scrollY.value = e.contentOffset.y;
});
// Parallax header
const headerStyle = useAnimatedStyle(() => ({
transform: [{
translateY: interpolate(scrollY.value, [0, 200], [0, -100], Extrapolation.CLAMP),
}],
opacity: interpolate(scrollY.value, [0, 200], [1, 0], Extrapolation.CLAMP),
}));
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
<Animated.View style={headerStyle}>
<Text>Parallax Header</Text>
</Animated.View>
</Animated.ScrollView>
Zoom Transitions (Expo Router, iOS 18+)
import { Link } from "expo-router";
<Link href="/detail" asChild>
<Link.AppleZoom>
<Pressable>
<Image source={thumbnail} />
</Pressable>
</Link.AppleZoom>
</Link>
Adding Animations to State Changes
// ✓ Always add entering/exiting for state-driven UI changes
{isVisible && (
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)}>
<Toast message={message} />
</Animated.View>
)}
// ✓ AnimatedFlatList for list item changes
import Animated from "react-native-reanimated";
const AnimatedFlashList = Animated.createAnimatedComponent(FlashList);
Common Mistakes
| Wrong | Right |
|---|---|
Animate width/height |
Animate transform: scaleX/scaleY |
Inline JS math in useAnimatedStyle |
useDerivedValue for computations |
Pressable inside GestureDetector |
Gesture.Tap() |
async in worklet |
Run async outside, update sharedValue in callback |
Frequent console.log in worklet |
console.log works but serializes to JS thread — use sparingly in hot paths |