Loading...
Loading...
Compare original and translation side by side
Reanimated 4.x / Worklets 0.7.x / React Native 0.80+ (New Architecture required)
适配 Reanimated 4.x / Worklets 0.7.x / React Native 0.80+(需启用新架构)
babel.config.jsuseStateuseSharedValuewithTimingwithSpring.onUpdate.onEndcancelAnimation(sv)velocitywithSpring<GestureHandlerRootView style={{ flex: 1 }}>useEffectReduceMotion.SystemuseReducedMotion()babel.config.jsuseStateuseSharedValue.onUpdatewithTimingwithSpring.onEndcancelAnimation(sv)velocitywithSpring<GestureHandlerRootView style={{ flex: 1 }}>useEffectReduceMotion.SystemuseReducedMotion()react-native-reanimatedreact-native-gesture-handler| File | Line | Issue | Severity | Fix |
|------|------|-------|----------|-----|
| src/Card.tsx | 12 | useState driving animation | HIGH | Replace with useSharedValue |
| src/Sheet.tsx | 45 | Missing cancelAnimation in onBegin | HIGH | Add cancelAnimation(sv) |
| src/List.tsx | 78 | Math.random() as key | MEDIUM | Use stable unique ID |react-native-reanimatedreact-native-gesture-handler| 文件 | 行号 | 问题 | 严重程度 | 修复方案 |
|------|------|-------|----------|-----|
| src/Card.tsx | 12 | 使用useState驱动动画 | 高 | 替换为useSharedValue |
| src/Sheet.tsx | 45 | 手势开始时缺少cancelAnimation | 高 | 添加cancelAnimation(sv) |
| src/List.tsx | 78 | 使用Math.random()作为key | 中 | 使用稳定的唯一ID |useStatesetStateuseSharedValuewithTimingwithSpring.onUpdate.valueuseAnimatedStylecancelAnimation.onBeginvelocitywithSpring.onEnd{ velocity: e.velocityX }elevationMath.random()Math.min()GestureHandlerRootViewuseEffectrunOnJSuseAnimatedReaction'worklet'borderWidthshadowOffsetshadowRadiusoverflow: 'hidden'borderRadiusReduceMotion.SystemuseReducedMotion()Animated.FlatListskipEnteringExitingAnimationsuseStatesetStateuseSharedValue.onUpdatewithTimingwithSpringsv.valueuseAnimatedStylewithTiming()useEffect.onBegincancelAnimation.onEndwithSpringvelocity{ velocity: e.velocityX }elevationMath.random()Math.min()GestureHandlerRootViewuseEffectuseAnimatedReactionrunOnJS'worklet'borderWidthshadowOffsetshadowRadiusborderRadiusoverflow: 'hidden'ReduceMotion.SystemuseReducedMotion()Animated.FlatListskipEnteringExitingAnimationsnpm install react-native-reanimated react-native-worklets react-native-gesture-handler// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-reanimated/plugin'], // MUST be last
};npx react-native start --reset-cachepod installgradlew cleannpm install react-native-reanimated react-native-worklets react-native-gesture-handler// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-reanimated/plugin'], // 必须放在最后
};npx react-native start --reset-cachepod installgradlew cleanconst offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
const animate = () => {
offset.value = withSpring(100, { damping: 15, stiffness: 150 });
};
<Animated.View style={[styles.box, animatedStyle]} />;const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
const animate = () => {
offset.value = withSpring(100, { damping: 15, stiffness: 150 });
};
<Animated.View style={[styles.box, animatedStyle]} />;| Context | Use | Why |
|---|---|---|
| During gesture (finger down) | Direct assignment | Follows finger at 60fps |
| Gesture release | | Natural physics feel |
| Fixed-duration transition | | Predictable timing |
| Momentum/fling | | Natural deceleration |
| Toggle state | | Bouncy feedback |
| 场景 | 使用函数 | 原因 |
|---|---|---|
| 手势操作中(手指按下时) | 直接赋值 | 以60fps跟踪手指移动 |
| 手势释放时 | | 获得符合物理规律的自然效果 |
| 固定时长过渡 | | 可预测的计时效果 |
| 动量/甩动效果 | | 自然减速效果 |
| 状态切换 | | 带弹性的反馈效果 |
const SPRING = {
gentle: { damping: 15, stiffness: 50 }, // Slow, smooth
normal: { damping: 10, stiffness: 100 }, // Balanced
snappy: { damping: 20, stiffness: 300 }, // Quick, snappy
noBounce:{ damping: 30, stiffness: 200 }, // Critically damped
};const SPRING = {
gentle: { damping: 15, stiffness: 50 }, // 缓慢、平滑
normal: { damping: 10, stiffness: 100 }, // 平衡
snappy: { damping: 20, stiffness: 300 }, // 快速、干脆
noBounce:{ damping: 30, stiffness: 200 }, // 临界阻尼(无弹跳)
};const offsetX = useSharedValue(0);
const startX = useSharedValue(0);
const pan = Gesture.Pan()
.onBegin(() => {
cancelAnimation(offsetX);
startX.value = offsetX.value;
})
.onUpdate((e) => {
offsetX.value = startX.value + e.translationX; // Direct assignment
})
.onEnd((e) => {
offsetX.value = withSpring(0, { velocity: e.velocityX, damping: 15 });
});const offsetX = useSharedValue(0);
const startX = useSharedValue(0);
const pan = Gesture.Pan()
.onBegin(() => {
cancelAnimation(offsetX);
startX.value = offsetX.value;
})
.onUpdate((e) => {
offsetX.value = startX.value + e.translationX; // 直接赋值
})
.onEnd((e) => {
offsetX.value = withSpring(0, { velocity: e.velocityX, damping: 15 });
});| Anti-Pattern | Fix |
|---|---|
| Replace with |
| Direct assign: |
Reading | Use |
Creating | Move to event handler / |
Missing | Add |
| Missing velocity on spring after gesture | Add |
| Shadow animation on Android | Use |
| Use stable unique IDs |
100+ items with | Cap delay: |
| 反模式 | 修复方案 |
|---|---|
使用 | 替换为 |
在 | 直接赋值: |
在JSX渲染中读取 | 使用 |
在render返回值中创建 | 移至事件处理函数/ |
手势开始时缺少 | 在 |
| 手势结束后弹簧动画缺少velocity参数 | 添加 |
| Android端动画阴影属性 | 改用 |
使用 | 使用稳定的唯一ID |
100+个元素使用 | 限制延迟: |
elevationoverflow: 'hidden'borderRadiusborderWidthshadowOffsetshadowRadiuselevationoverflow: 'hidden'borderRadiusborderWidthshadowOffsetshadowRadiusshadowOpacityshadowRadiusshadowOffsetenableLayoutAnimations(true)shadowOpacityshadowRadiusshadowOffsetenableLayoutAnimations(true)useEffect(() => {
return () => {
cancelAnimation(offset);
cancelAnimation(scale);
};
}, []);runOnJSconst isMounted = useSharedValue(true);
useEffect(() => () => { isMounted.value = false; }, []);
useAnimatedReaction(
() => progress.value,
(val) => {
if (val >= 1 && isMounted.value) runOnJS(onComplete)();
}
);useEffect(() => {
return () => {
cancelAnimation(offset);
cancelAnimation(scale);
};
}, []);runOnJSuseAnimatedReactionconst isMounted = useSharedValue(true);
useEffect(() => () => { isMounted.value = false; }, []);
useAnimatedReaction(
() => progress.value,
(val) => {
if (val >= 1 && isMounted.value) runOnJS(onComplete)();
}
);'worklet'useAnimatedStyleuseAnimatedPropsuseDerivedValueuseAnimatedReactionuseAnimatedScrollHandlerlocalStoragerunOnJS(fn)(args)runOnUI(fn)(args)'worklet'useAnimatedStyleuseAnimatedPropsuseDerivedValueuseAnimatedReactionuseAnimatedScrollHandlerlocalStoragerunOnJS(fn)(args)runOnUI(fn)(args)<Animated.View
entering={FadeInUp.delay(index * 50).springify().damping(12)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
/>delay(index * 50)Math.random()skipEnteringExitingAnimationsAnimated.FlatListentering={reducedMotion ? undefined : FadeIn}<Animated.View
entering={FadeInUp.delay(index * 50).springify().damping(12)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
/>delay(index * 50)Math.random()Animated.FlatListskipEnteringExitingAnimationsentering={reducedMotion ? undefined : FadeIn}--reset-cachepod installgradlew clean'worklet'GestureHandlerRootView.value--reset-cachepod installgradlew clean'worklet'GestureHandlerRootView.valuewithTimingwithSpringwithDecaywithDelaywithRepeatwithSequencewithClampuseSharedValueuseAnimatedStyleuseAnimatedPropsuseDerivedValueuseAnimatedReactionuseAnimatedRefuseAnimatedScrollHandleruseScrollViewOffsetuseAnimatedKeyboarduseAnimatedSensoruseFrameCallbackrunOnJSrunOnUIcancelAnimationinterpolateinterpolateColorclamp'worklet'runOnUIrunOnJSrunOnUISyncSimultaneousExclusiveRacewithTimingwithSpringwithDecaywithDelaywithRepeatwithSequencewithClampuseSharedValueuseAnimatedStyleuseAnimatedPropsuseDerivedValueuseAnimatedReactionuseAnimatedRefuseAnimatedScrollHandleruseScrollViewOffsetuseAnimatedKeyboarduseAnimatedSensoruseFrameCallbackrunOnJSrunOnUIcancelAnimationinterpolateinterpolateColorclamp'worklet'runOnUIrunOnJSrunOnUISyncSimultaneousExclusiveRace