mcpField
R
reactiive

reactiive

github.com/enzomanuelmangano设计与 UI1041 次安装31 周前更新
代码仓库

关于 reactiive

reactiive 是帮助 React Native 开发者构建动画的技能,内容涵盖 Reanimated 的共享值、手势、弹簧效果和布局动画。它还提供动画设计原则与常见问题指引。

主要功能

  1. 01使用 Reanimated 共享值与动画样式
  2. 02通过派生值组织动画状态
  3. 03响应共享值变化执行回调
  4. 04设计手势与弹簧动画
  5. 05实现布局和共享元素过渡
  6. 06排查变换顺序等常见问题

适用场景

  1. 01为触控操作添加反馈动画
  2. 02构建拖动和滑动交互
  3. 03制作元素进出场及布局动画
  4. 04设计跨屏共享元素过渡

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/enzomanuelmangano/skill --skill reactiive

安装后在 Claude Code 中输入/reactiive确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

design-engineering.md
easings.md
gestures.md
scroll-animations.md
sequences.md
skia-patterns.md
springs.md
text-animations.md
worklets.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,
});
尽早确定约束
  • 整个应用使用三种弹簧配置
  • 触摸反馈只使用缩放和不透明度
  • 所有时间动画统一使用一种缓动曲线

一次性做出决定,并在各处应用。应用因此显得协调统一。


参考文件


资源

评论

支持 Markdown