Files
minimax-skills/skills/react-native-dev/references/animations.md

255 lines
6.7 KiB
Markdown
Raw Normal View History

2026-03-26 20:32:52 +08:00
# Animations Reference
Reanimated 3 animations, gestures, and transitions for Expo/React Native.
## Core Rules
- **Only animate `transform` and `opacity`** — GPU-composited, no layout recalculation
- Use `useDerivedValue` for computed animated values, not inline JS expressions
- Use `Gesture.Tap` instead of `Pressable` inside `GestureDetector`
- All Reanimated callbacks run as worklets on the UI thread — no async/await
## Setup
```bash
npx expo install react-native-reanimated react-native-gesture-handler
```
```js
// babel.config.js
module.exports = { presets: ["babel-preset-expo"], plugins: ["react-native-reanimated/plugin"] };
```
```tsx
// 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
```tsx
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
```tsx
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
```tsx
import { useDerivedValue } from "react-native-reanimated";
const progress = useSharedValue(0); // 01
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
```tsx
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
```tsx
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>
```
```tsx
// 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
```tsx
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+)
```tsx
import { Link } from "expo-router";
<Link href="/detail" asChild>
<Link.AppleZoom>
<Pressable>
<Image source={thumbnail} />
</Pressable>
</Link.AppleZoom>
</Link>
```
## Adding Animations to State Changes
```tsx
// ✓ 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 |