计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
react-native-reanimated【效率与协作】React Native Reanimated is an animation library for React Native that runs animations on the UI thread through worklets and shared values, so they stay smooth while the JavaScript thread is busy. Use when someone asks to "animate in React Native", "Reanimated", "smooth mobile animations", "gesture animations", "shared element transitions", "60fps Rea…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Reanimated runs animations on the native UI thread, so they stay at 60fps (or the display's refresh rate) even when the JavaScript thread is busy. It uses "worklets": small JavaScript functions that execute on the UI thread via JSI. It is the standard for production-quality animations in React Native: gesture-driven interactions, layout transitions, scroll-based effects, and shared element transitions.
This skill targets Reanimated 4 (4.7.0, September 2026). Version 4 works only on the New Architecture, moves worklets into the separate react-native-worklets package, and adds CSS-style transitions and animations. Apps still on the old architecture stay on 3.x, which is no longer actively maintained.
# Expo: installs the versions pinned by the SDK (SDK 57: Reanimated 4.5, Worklets 0.10, Gesture Handler 2.32)
npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler
npx expo prebuild # development builds only: regenerates ios/ and android/
# React Native Community CLI
npm install react-native-reanimated react-native-worklets react-native-gesture-handler
cd ios && pod install && cd ..
npm start -- --reset-cache
babel-preset-expo adds the Worklets Babel plugin by itself; do not list it in babel.config.js.'react-native-worklets/plugin' as the last entry of plugins in babel.config.js. The Reanimated 3 name react-native-reanimated/plugin still resolves in 4.x but is the legacy spelling.react-native-worklets and React Native versions (4.7 needs Worklets 0.13 and RN 0.86–0.88); check the compatibility table before a manual upgrade.GestureHandlerRootView from react-native-gesture-handler with style={{ flex: 1 }}.// components/FadeIn.tsx — Basic animation with shared values
import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSpring } from "react-native-reanimated";
import { type ReactNode, useEffect } from "react";
export function FadeInCard({ children }: { children: ReactNode }) {
const opacity = useSharedValue(0);
const translateY = useSharedValue(20);
useEffect(() => {
opacity.value = withTiming(1, { duration: 600 });
translateY.value = withSpring(0);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateY: translateY.value }],
}));
return <Animated.View style={[{ backgroundColor: "white", padding: 16 }, animatedStyle]}>{children}</Animated.View>;
}
Read and write .value only in effects, event handlers and worklets, never during render. With the React Compiler use opacity.get() and opacity.set(1) instead of .value.
// components/SwipeToDelete.tsx — Swipe gesture with animation
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { scheduleOnRN } from "react-native-worklets";
import type { ReactNode } from "react";
export function SwipeToDelete({ onDelete, children }: { onDelete: () => void; children: ReactNode }) {
const translateX = useSharedValue(0);
const pan = Gesture.Pan()
.onUpdate((event) => {
translateX.value = Math.min(0, event.translationX); // only allow left swipe
})
.onEnd((event) => {
if (event.translationX < -150) {
translateX.value = withSpring(-400);
scheduleOnRN(onDelete); // call a JS-thread function from the UI thread
} else {
translateX.value = withSpring(0); // snap back
}
});
const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }] }));
return (
<GestureDetector gesture={pan}>
<Animated.View style={animatedStyle}>{children}</Animated.View>
</GestureDetector>
);
}
scheduleOnRN(fn, ...args) replaces runOnJS(fn)(...args), which is deprecated in 4.x. Gesture.Pan() is the Gesture Handler 2 builder API, the one Expo SDK 57 installs; Gesture Handler 3 keeps it and adds hooks (usePanGesture({ onUpdate, onDeactivate }), where onStart/onEnd are named onActivate/onDeactivate).
// components/AnimatedList.tsx — Items animate in/out automatically
import Animated, { FadeInDown, FadeOutLeft, LinearTransition } from "react-native-reanimated";
import { Pressable, Text } from "react-native";
type Item = { id: string; title: string };
export function AnimatedList({ items, onRemove }: { items: Item[]; onRemove: (id: string) => void }) {
return (
<Animated.FlatList
data={items}
keyExtractor={(item) => item.id}
itemLayoutAnimation={LinearTransition} // remaining rows slide into place; single column only
renderItem={({ item, index }) => (
<Animated.View
entering={FadeInDown.delay(index * 100).springify()}
exiting={FadeOutLeft.duration(300)}
style={{ backgroundColor: "white", padding: 16, marginVertical: 4, borderRadius: 8 }}
>
<Text>{item.title}</Text>
<Pressable onPress={() => onRemove(item.id)}><Text style={{ color: "#dc2626" }}>Remove</Text></Pressable>
</Animated.View>
)}
/>
);
}
// components/ParallaxHeader.tsx — Parallax effect on scroll
import Animated, {
useAnimatedScrollHandler, useSharedValue, useAnimatedStyle, interpolate, Extrapolation,
} from "react-native-reanimated";
import { Text } from "react-native";
export function ParallaxHeader() {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => { scrollY.value = event.contentOffset.y; },
});
const headerStyle = useAnimatedStyle(() => ({
height: interpolate(scrollY.value, [-100, 0, 200], [400, 300, 100], Extrapolation.CLAMP),
opacity: interpolate(scrollY.value, [0, 200], [1, 0.3], Extrapolation.CLAMP),
}));
return (
<>
<Animated.View style={[{ backgroundColor: "#2563eb" }, headerStyle]}>
<Text style={{ color: "white", fontSize: 28, fontWeight: "bold" }}>Trail Log</Text>
</Animated.View>
<Animated.ScrollView onScroll={scrollHandler}>{/* Content */}</Animated.ScrollView>
</>
);
}
When only the offset is needed, useScrollOffset(animatedRef) (named useScrollViewOffset in 3.x) returns it as a shared value without a handler.
// a state change animates by itself: no shared value, no hook
<Animated.View
style={{
height: 80,
width: expanded ? 240 : 120,
backgroundColor: expanded ? "#16a34a" : "#2563eb",
transitionProperty: ["width", "backgroundColor"],
transitionDuration: 300,
}}
/>
// the same tag on two screens of a native stack animates the element between them
<Animated.Image sharedTransitionTag="trail-42-photo" source={{ uri: photoUrl }} style={{ width: 120, height: 120 }} />
Shared element transitions are experimental in 4.x and off by default. Enable them with "reanimated": { "staticFeatureFlags": { "ENABLE_SHARED_ELEMENT_TRANSITIONS": true } } in the app's package.json (4.2+), then rebuild the native app; static flags cannot be changed in Expo Go.
User prompt: "Build a Tinder-style card swipe interface with smooth animations."
// components/SwipeCard.tsx
import Animated, { interpolate, useAnimatedStyle, useSharedValue, withSpring, withTiming } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { scheduleOnRN } from "react-native-worklets";
import { useWindowDimensions } from "react-native";
import type { ReactNode } from "react";
type Props = { children: ReactNode; onSwipe: (direction: "left" | "right") => void };
export function SwipeCard({ children, onSwipe }: Props) {
const { width } = useWindowDimensions();
const x = useSharedValue(0);
const y = useSharedValue(0);
const pan = Gesture.Pan()
.onUpdate((e) => { x.value = e.translationX; y.value = e.translationY; })
.onEnd((e) => {
if (Math.abs(e.translationX) > width * 0.3) {
const direction = e.translationX > 0 ? "right" : "left";
x.value = withTiming(Math.sign(e.translationX) * width * 1.5, { duration: 200 }, (finished) => {
if (finished) scheduleOnRN(onSwipe, direction); // remove the card in React state
});
} else {
x.value = withSpring(0);
y.value = withSpring(0);
}
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }, { translateY: y.value }, { rotate: `${interpolate(x.value, [-width, 0, width], [-15, 0, 15])}deg` }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={style}>{children}</Animated.View>
</GestureDetector>
);
}
Result: the card follows the finger and tilts up to 15°. Released past 30% of the screen width it flies off and onSwipe("left" | "right") runs on the JS thread; otherwise it springs back to the centre.
User prompt: "Create a bottom sheet that can be dragged up and snaps to positions."
// components/BottomSheet.tsx
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { useWindowDimensions } from "react-native";
import type { ReactNode } from "react";
export function BottomSheet({ children }: { children: ReactNode }) {
const { height } = useWindowDimensions();
const snapPoints = [height * 0.1, height * 0.5, height * 0.85]; // top edge: open, half, peek
const top = useSharedValue(snapPoints[2]);
const start = useSharedValue(0);
const pan = Gesture.Pan()
.onBegin(() => { start.value = top.value; })
.onUpdate((e) => { top.value = Math.max(snapPoints[0], start.value + e.translationY); })
.onEnd((e) => {
const projected = top.value + e.velocityY * 0.2; // where the fling would land
const target = snapPoints.reduce((a, b) => (Math.abs(b - projected) < Math.abs(a - projected) ? b : a));
top.value = withSpring(target, { velocity: e.velocityY });
});
const style = useAnimatedStyle(() => ({ transform: [{ translateY: top.value }] }));
const sheet = { position: "absolute", left: 0, right: 0, top: 0, height, backgroundColor: "white" } as const;
return (
<GestureDetector gesture={pan}>
<Animated.View style={[sheet, style]}>{children}</Animated.View>
</GestureDetector>
);
}
Result: the sheet starts in the peek position, follows the drag, and on release springs to the nearest of the three snap points, taking the fling velocity into account.
useSharedValue lives on the UI thread; changing it does not re-render the componentuseAnimatedStyle for dynamic styles — recalculated on the UI threadwithSpring for natural motion — withTiming for precise duration. Version 4 changed the spring defaults (damping 120, stiffness 900) and treats duration as perceptual; Reanimated3DefaultSpringConfig restores the old feelscheduleOnRN to call JS from worklets — pass a function defined in the component or module scope, not one created inside the workletGesture.Pan(), Gesture.Pinch(), etc.; callbacks passed inline are workletized automatically, a callback stored in a variable needs a 'worklet'; directiveentering, exiting props on Animated componentsinterpolate for value mapping — map scroll position to opacity, scale, etc.; pass Extrapolation.CLAMP to stop at the range endsreact-native-worklets, rename the Babel plugin, and replace useAnimatedGestureHandler, useWorkletCallback and combineTransition, which were removedAnimated API (CSS transitions on native come from Reanimated itself); Reanimated 4 cannot be installed in an app on the old architecture