关于 reactiive
reactiive 是帮助 React Native 开发者构建动画的技能,内容涵盖 Reanimated 的共享值、手势、弹簧效果和布局动画。它还提供动画设计原则与常见问题指引。
主要功能
- 01使用 Reanimated 共享值与动画样式
- 02通过派生值组织动画状态
- 03响应共享值变化执行回调
- 04设计手势与弹簧动画
- 05实现布局和共享元素过渡
- 06排查变换顺序等常见问题
适用场景
- 01为触控操作添加反馈动画
- 02构建拖动和滑动交互
- 03制作元素进出场及布局动画
- 04设计跨屏共享元素过渡
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ enzomanuelmangano/ skill --skill reactiive安装后在 Claude Code 中输入/reactiive确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
design-engineering.mdeasings.mdgestures.mdscroll-animations.mdsequences.mdskia-patterns.mdsprings.mdtext-animations.mdworklets.md详细介绍
React Native 动画技巧
初次响应
首次调用此技能且没有具体问题时,只回复:
我已准备好帮你构建恰到好处的动画。
在用户提问前,不要提供任何其他信息。
配方
每个动画都需要三种要素。缺少任何一种,都会导致问题。
// 1. A shared value that lives on the UI thread
const scale = useSharedValue(1);
// 2. An animated style that reads from it
const rStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.get() }],
}));
// 3. An Animated.View that renders it
<Animated.View style={[styles.square, rStyle]} />
共享值保存数据。动画样式将数据转换为属性。Animated.View 将其渲染出来。
理解这一点后,其他内容就只是不同的变化形式。
注意: 使用 .get() 和 .set(),而不是 .value,以兼容 React Compiler。
四个 Hook
| Hook | 用途 |
|---|---|
| useSharedValue | 在 UI 线程上创建一个值 |
| useAnimatedStyle | 将共享值转换为样式 |
| useDerivedValue | 根据现有值计算新值 |
| useAnimatedReaction | 在值变化时触发副作用 |
useDerivedValue 是我最喜欢的 Hook——它会非常重要。
useDerivedValue
定义一个新的共享值,使其根据其他共享值自动重新计算。
const isDragging = useSharedValue(false);
const rotate = useDerivedValue(() => {
return withSpring(isDragging.get() ? '45deg' : '0deg');
});
const scale = useDerivedValue(() => {
return withSpring(isDragging.get() ? 0.9 : 1);
});
诀窍:用动画函数包装返回值。isDragging 变化时,派生值会平滑地进行动画过渡。
链式派生值
const color = useDerivedValue(() => {
if (isDragging.get()) return '#0099ff';
if (translateY.get() < 0) return 'black';
if (translateY.get() > 0) return 'white';
return '#0099ff';
});
const animatedColor = useDerivedValue(() => {
return withTiming(color.get());
});
将它们串联起来。颜色由状态派生。animatedColor 再添加动画。简洁又声明式。
useAnimatedReaction
它就像 UI 线程上的 useEffect。它会监听共享值,并在值变化时触发回调。
useAnimatedReaction(
() => left.get(),
(curr, prev) => {
if (curr !== prev && curr !== 0) {
cancelAnimation(scale);
scale.set(0);
scale.set(withSpring(1, { mass: 0.5 }));
}
},
);
检查 curr !== 0 可避免在挂载时触发。启动新动画前,先取消所有正在运行的动画。
常见错误
变换顺序
变换的顺序很重要。先缩放再平移会放大平移距离。
// ✗ Wrong: translateX of 50 becomes 50 * 1.5 = 75
transform: [
{ scale: 1.5 },
{ translateX: 50 },
]
// ✓ Correct: translate first, then scale
transform: [
{ translateX: translateX.get() },
{ translateY: translateY.get() },
{ scale: scale.get() },
{ rotate: rotate.get() },
]
数组索引作为键
使用数组索引作为键会破坏布局动画。
// ✗ Wrong: indices shift when items change
{items.map((item, index) => (
<Animated.View key={index} layout={LinearTransition} />
))}
// ✓ Correct: stable unique IDs
{items.map((item) => (
<Animated.View key={item.id} layout={LinearTransition} />
))}
iOS 回弹
iOS 上的滚动视图会超出边界回弹。如果不进行钳制,插值就会出错。
import { clamp } from 'react-native-reanimated';
// Raw progress can go negative or exceed 1
const rawProgress = offset / maxOffset;
// Clamped progress stays valid
progress.set(clamp(rawProgress, 0, 1));
变换使用 Timing
变换上的 Timing 动画会显得很机械。请使用弹簧动画。
// ✗ Mechanical
scale.set(withTiming(1.2, { duration: 300 }));
// ✓ Natural
scale.set(withSpring(1.2));
缺少速度
结束手势时,请传入速度。否则,动画会显得脱节。
.onFinalize((event) => {
translateX.set(withSpring(0, {
velocity: event.velocityX,
}));
});
弹簧动画
弹簧动画模拟物理效果,因此感觉自然。真实物体不会以固定时长移动。
dampingRatio
我的建议范围:0.95 - 1。 专业 UI 请保持在此范围内。
| dampingRatio | 行为 |
|---|---|
| 0.95 - 1 | 临界阻尼——无过冲 |
| < 0.95 | 欠阻尼——有意实现弹跳效果 |
| > 1 | 过阻尼——迟缓,应避免 |
// Default: clean, professional
scale.set(withSpring(1, { dampingRatio: 1 }));
// Slightly softer feel
translateY.set(withSpring(0, { dampingRatio: 0.95 }));
如何选择
| 动画类型 | 使用 |
|---|---|
| 变换(缩放、平移、旋转) | withSpring |
| 静态属性(不透明度、颜色) | withTiming + 缓动 |
// ✓ Transforms: springs
scale.set(withSpring(1.1));
// ✓ Opacity: timing
opacity.set(withTiming(1, { duration: 200 }));
Context 模式
平移手势在新的触摸开始时会跳回起点。解决方法:在手势开始时保存位置。
const translateX = useSharedValue(0);
const context = useSharedValue({ x: 0 });
const panGesture = Gesture.Pan()
.onBegin(() => {
context.set({ x: translateX.get() });
})
.onUpdate((event) => {
translateX.set(event.translationX + context.get().x);
});
使用 pressto 提供触摸反馈
每次轻触都需要反馈。pressto 提供了动画触摸组件,使用起来恰到好处。
import { PressableScale } from 'pressto';
<PressableScale onPress={handlePress}>
<View style={styles.button}>
<Text>Tap me</Text>
</View>
</PressableScale>
全局触觉反馈
import { PressablesConfig } from 'pressto';
import * as Haptics from 'expo-haptics';
<PressablesConfig
globalHandlers={{
onPress: () => Haptics.selectionAsync(),
}}
>
{children}
</PressablesConfig>
布局动画
这是实现挂载和卸载动画最简单的方法。
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
{isVisible && (
<Animated.View
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(200)}
layout={LinearTransition.springify()}
/>
)}
手动构建退出动画很困难。Reanimated 会自动处理延迟、动画和卸载。
LinearTransition.springify()
// Snappy
LinearTransition.springify().mass(0.3).damping(20).stiffness(250)
// Smooth
LinearTransition.springify().mass(1).damping(20).stiffness(120)
共享过渡
在屏幕之间导航时,让元素在两者之间变形过渡。
import { Image } from 'expo-image';
import Animated from 'react-native-reanimated';
const AnimatedImage = Animated.createAnimatedComponent(Image);
// Home screen
<AnimatedImage
source={{ uri: item.url }}
sharedTransitionTag={`image-${item.id}`}
style={{ width: 100, height: 100 }}
/>
// Detail screen - same tag
<AnimatedImage
source={{ uri: item.url }}
sharedTransitionTag={`image-${item.id}`}
style={{ width: '100%', aspectRatio: 1 }}
/>
详情屏幕请使用 containedTransparentModal 展示模式。
魔法按钮
这就是令人难忘的动画效果:缩放、旋转渐变、模糊——轻触即可触发。
const isTouched = useSharedValue(false);
const scale = useDerivedValue(() => {
return withSpring(isTouched.get() ? 1.2 : 1);
});
const rotate = useDerivedValue(() => {
return withTiming(isTouched.get() ? Math.PI * 2 : 0, { duration: 1000 });
});
<Canvas style={{ width: realWidth, height: realHeight }}>
<Group origin={vec(centerX, centerY)} transform={[{ rotate: rotate.get() }]}>
<RoundedRect x={realX} y={realY} width={width} height={height} r={width / 2}>
<SweepGradient c={center} colors={['cyan', 'magenta', 'yellow', 'cyan']} />
<BlurMask blur={20} style="solid" />
</RoundedRect>
</Group>
</Canvas>
功能性动画用于确认触摸。令人难忘的动画带来惊喜。魔法按钮两者兼具。
功能性动画与令人难忘的动画
功能性动画是用户预期会有的效果。点按按钮,获得反馈。这些动画必不可少——没有它们,应用会让人觉得坏了。但它们不会让用户记住你的应用。
令人难忘的动画则是用户意料之外的效果。会发光并旋转的渐变、3D 翻转。这些动画能带来惊喜。
先实现功能性动画,再添加令人难忘的动画。
理念
每个动画都要有明确用途
| 用途 | 动画 |
|---|---|
| 确认触摸 | 按下时缩小 |
| 显示连接关系 | 卡片变形为详情视图 |
| 引导注意力 | 从触发方向淡入 |
| 保持空间方位感 | 在屏幕之间使用共享元素 |
| 表示进度 | 脉动或闪烁 |
如果你说不出动画的用途,就删掉它。
根据频率调整强度
某个效果出现得越频繁,就越应该保持含蓄。
// Happens constantly: clean, no bounce
const buttonScale = withSpring(pressed ? 0.97 : 1, {
dampingRatio: 1,
});
尽早确定约束
- 整个应用使用三种弹簧配置
- 触摸反馈只使用缩放和不透明度
- 所有时间动画统一使用一种缓动曲线
一次性做出决定,并在各处应用。应用因此显得协调统一。
参考文件
- springs.md - 物理效果、阻尼比、速度
- easings.md - 用于不透明度和颜色的曲线
- gestures.md - 平移、点按、上下文模式、pressto
- scroll-animations.md - 视差、进度、插值
- skia-patterns.md - 画布、着色器、路径、模糊
- sequences.md - 布局动画、关键帧、交错
- text-animations.md - ReText、AnimateableText、Skia 文本
- worklets.md - 线程处理、scheduleOnRN
- design-engineering.md - 理念、工艺
资源
- Reanimated 文档
- React Native Skia
- pressto - 动画触摸组件
- Redash - ReText、实用工具
- AnimateableText - 共享值文本