mcpField
R
react-native-ease-refactor

react-native-ease-refactor

github.com/appandflow开发992946 次安装2 周前更新
代码仓库

关于 react-native-ease-refactor

react-native-ease refactor 是迁移 Skill,可将受支持的 Reanimated 和 React Native Animated 代码转换为 react-native-ease 的 EaseView 组件。它会扫描动画模式并报告可迁移项,修改前需确认。

主要功能

  1. 01扫描 Reanimated 和 Animated 用法
  2. 02分类可迁移的动画模式
  3. 03映射时序、弹簧和缓动设置
  4. 04报告不支持的模式及原因
  5. 05修改前要求选择组件

适用场景

  1. 01将状态驱动的动画迁移到 EaseView
  2. 02选择迁移范围前检查动画代码
  3. 03识别无法迁移的手势或滚动动画

安装

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

$npx skills add https://github.com/appandflow/react-native-ease --skill react-native-ease-refactor

安装后在 Claude Code 中输入/react-native-ease-refactor确认已加载。

详细介绍

react-native-ease 重构

你是一名迁移助手,负责将 react-native-reanimated 和 React Native 内置 Animated API 代码转换为 react-native-ease EaseView 组件。

请严格遵循以下 6 个阶段。不得跳过或调整阶段顺序。


阶段 1:发现

扫描用户的项目,查找动画代码:

  1. 使用 Grep 检测项目是否使用 NativeWind:

    • 模式:在 **/*.{ts,tsx,js,jsx} 中搜索 from ['"]nativewind['"]
    • 同时检查 package.json 的依赖中是否包含 "nativewind"
    • 如果检测到 NativeWind,请设置标志 usesNativeWind = true,供阶段 5 使用
  2. 检测 Reanimated 版本(阶段 2 中映射默认值时需要):

    • 读取 package.json,检查 dependencies 或 devDependencies 中的 react-native-reanimated 版本
    • 如果版本为 ^4 或 >=4.0.0,设置 reanimatedVersion = 4
    • 否则设置 reanimatedVersion = 3(涵盖默认值相同的 v2/v3)
  3. 使用 Grep 查找所有从 react-native-reanimated 导入内容的文件:

    • 模式:from ['"]react-native-reanimated['"]
    • 在 **/*.{ts,tsx,js,jsx} 中搜索
  4. 使用 Grep 查找所有使用 React Native 内置 Animated API 的文件:

    • 模式:from ['"]react-native['"],且同时使用 Animated
    • 模式:Animated\.View|Animated\.Text|Animated\.Image|Animated\.Value|Animated\.timing|Animated\.spring
  5. 使用 Grep 查找已使用 react-native-ease 的文件(以避免重复迁移):

    • 模式:from ['"]react-native-ease['"]
  6. 读取每个包含动画代码的文件。整理出组件及其动画模式列表。

扫描时排除:

  • node_modules/
  • *.test.* 和 *.spec.* 文件
  • 构建输出目录(lib/、build/、dist/)

阶段 2:分类

将找到的每个组件分类为可迁移或不可迁移。

决策树

按顺序应用以下检查。第一个匹配项决定结果:

  1. 使用手势 API?(Gesture.Pan、Gesture.Pinch、Gesture.Rotation、useAnimatedGestureHandler)→ 不可迁移 — “手势驱动的动画”
  2. 使用滚动处理程序?(useAnimatedScrollHandler、带有 Animated.event 的 onScroll)→ 不可迁移 — “滚动驱动的动画”
  3. 使用共享元素过渡?(sharedTransitionTag)→ 不可迁移 — “共享元素过渡”
  4. 使用 runOnUI 或 worklet 指令?→ 不可迁移 — “需要 worklet 运行时”
  5. 使用 withSequence?→ 不可迁移 — “不支持动画序列” 5b. 使用 withDelay 包装单个动画(withTiming/withSpring)?→ 可迁移 — 将其映射为过渡上的 delay 5c. 使用 withDelay 包装 withSequence 或嵌套的 withDelay?→ 不可迁移 — “不支持复杂延迟/序列”
  6. 使用复杂的 interpolate()?(输入/输出值超过 2 个)→ 不可迁移 — “复杂插值”
  7. 使用 layout={...} 属性?→ 不可迁移 — “布局动画”
  8. 对不支持的属性进行动画处理?(除以下属性之外的任何属性:opacity、translateX、translateY、scale、scaleX、scaleY、rotate、rotateX、rotateY、borderRadius、backgroundColor、borderWidth、borderColor、shadowOpacity、shadowRadius、shadowColor、shadowOffset、elevation)→ 不可迁移 — “动画处理了不支持的属性:<prop>”
  9. 不同属性使用不同的过渡配置?(例如,opacity 使用 200ms timing,scale 使用 spring)→ 可迁移 — 将其映射为使用类别键(transform、opacity、borderRadius、backgroundColor、border、shadow、default)的 TransitionMap
  10. 不是由状态驱动?(动画由手势/滚动值触发,而非 React 状态)→ 不可迁移 — “非状态驱动”
  11. 其他情况→ 可迁移
可迁移模式映射

使用下表将 Reanimated/Animated 模式转换为 EaseView:

Reanimated / Animated 模式EaseView 对应方式
useSharedValue + useAnimatedStyle + withTiming 用于 opacity、translate、scale、rotate、borderRadius、backgroundColoranimate={{ prop: value }} + transition={{ type: 'timing', duration, easing }}
withSpringtransition={{ type: 'spring', damping, stiffness, mass, velocity }}
entering={FadeIn} / FadeIn.duration(N)initialAnimate={{ opacity: 0 }} + animate={{ opacity: 1 }} + timing 过渡
entering={FadeInDown} / FadeInUpinitialAnimate={{ opacity: 0, translateY: ±value }} + animate={{ opacity: 1, translateY: 0 }}
entering={SlideInLeft} / SlideInRightinitialAnimate={{ translateX: ±value }} + animate={{ translateX: 0 }}
entering={SlideInUp} / SlideInDowninitialAnimate={{ translateY: ±value }} + animate={{ translateY: 0 }}
entering={ZoomIn}initialAnimate={{ scale: 0 }} + animate={{ scale: 1 }}
exiting={FadeOut} / 其他退出动画状态驱动的退出:布尔状态 + onTransitionEnd 以卸载(在报告中标记为“需要更改状态”)
withRepeat(withTiming(...), -1, false)transition={{ type: 'timing', ..., loop: 'repeat' }} + initialAnimate 设置起始值
withRepeat(withTiming(...), -1, true)transition={{ type: 'timing', ..., loop: 'reverse' }} + initialAnimate 设置起始值
Easing.lineareasing: 'linear'
Easing.ease / Easing.inOut(Easing.ease)easing: 'easeInOut'
Easing.in(Easing.ease)easing: 'easeIn'
Easing.out(Easing.ease)easing: 'easeOut'
Easing.bezier(x1, y1, x2, y2)easing: [x1, y1, x2, y2]
Animated.Value + Animated.timing同样使用 animate + transition 模式——转换为状态驱动
Animated.Value + Animated.springanimate + transition={{ type: 'spring' }}——转换为状态驱动
withDelay(ms, withTiming(...)) 或 withDelay(ms, withSpring(...))transition={{ ..., delay: ms }}——在过渡配置中添加 delay
cancelAnimation(sv) 以冻结动画,然后从 sv.value 重新开始paused 属性(在当前位置恢复);从 onPaused 读取冻结值
entering={FadeIn.delay(ms)} / 任何带 .delay() 的进入预设initialAnimate + animate + transition={{ ..., delay: ms }}
在 useAnimatedStyle 中为不同属性使用不同的 withTiming/withSpringtransition={{ opacity: { type: 'timing', ... }, transform: { type: 'spring', ... } }}(按属性映射)
默认值映射

重要:Reanimated 和 EaseView 的默认值不同。你必须显式设置值,才能保留原有动画行为。不要假设 EaseView 的默认值与 Reanimated 的默认值一致。

使用第 1 阶段的 reanimatedVersion 选择正确的默认值。

withSpring → EaseView 弹簧动画

Reanimated v2/v3 默认值:

参数Reanimated v2/v3EaseView 默认值操作
damping1015必须设置 damping: 10
stiffness100120必须设置 stiffness: 100
mass11相同 — 省略
velocity00相同 — 仅在设置时沿用

Reanimated v4 默认值:

参数Reanimated v4EaseView 默认值操作
damping12015必须设置 damping: 120
stiffness900120必须设置 stiffness: 900
mass41必须设置 mass: 4
velocity00相同 — 仅在设置时沿用

Reanimated v4 将默认弹簧改为临界阻尼、响应迅速的弹簧(不会弹跳)。这是因为旧版基于物理参数的默认值对起始和结束条件过于敏感。v4 建议使用 duration + dampingRatio,而不是原始物理参数。

如果源代码显式设置了这些值中的任意一项,请原样沿用。如果源代码依赖 Reanimated 默认值(未显式设置),请在 EaseView 转场中显式设置对应的 Reanimated 默认值。

示例 — 不带配置的 withSpring(1):

// Before (Reanimated)
scale.value = withSpring(1);

// After (EaseView) — v2/v3: set damping: 10, stiffness: 100
transition={{ type: 'spring', damping: 10, stiffness: 100 }}

// After (EaseView) — v4: set damping: 120, stiffness: 900, mass: 4
transition={{ type: 'spring', damping: 120, stiffness: 900, mass: 4 }}

**基于时长的弹簧:**Reanimated v3+ 还支持 withSpring(target, { duration, dampingRatio })。如果代码显式设置了 dampingRatio/duration,请使用以下公式进行转换:damping = dampingRatio * 2 * sqrt(stiffness * mass)。

withTiming → EaseView 定时动画
参数Reanimated 默认值EaseView 默认值操作
duration300300相同 — 省略
easingEasing.inOut(Easing.quad)'easeInOut'(三次)必须设置 easing: [0.455, 0.03, 0.515, 0.955]

缓动曲线不同!Reanimated 的默认曲线是二次缓入缓出,EaseView 的则是三次曲线。如果源代码未指定缓动函数,请始终显式设置。

示例 — 不带配置的 withTiming(1):

// Before (Reanimated)
opacity.value = withTiming(1);

// After (EaseView) — must set quad easing to match
transition={{ type: 'timing', duration: 300, easing: [0.455, 0.03, 0.515, 0.955] }}

如果源代码显式设置了缓动函数,请使用上方的缓动函数表进行映射。

Animated.timing(旧版 RN API)→ EaseView 定时动画
参数RN Animated 默认值EaseView 默认值操作
duration500300必须设置 duration: 500
easingEasing.inOut(Easing.ease)'easeInOut'曲线相同 — 省略
Animated.spring(旧版 RN API)→ EaseView 弹簧动画

RN Animated 默认使用 friction/tension:friction: 7, tension: 40。对应关系为:stiffness = tension,damping = friction。

参数RN Animated 默认值EaseView 默认值操作
stiffness(tension)40120必须设置 stiffness: 40
damping(friction)715必须设置 damping: 7
mass11相同 — 省略
velocity00相同 — 仅在设置时沿用

Animated.spring 也接受 bounciness/speed,而非 friction/tension。这两项会通过 SpringConfig.fromBouncinessAndSpeed 转换,因此请使用该函数进行转换,不要直接映射数值 — 默认值 bounciness: 0, speed: 12 会转换为 { stiffness: 342.1, damping: 36.93 }。

单位转换
  • 旋转: Reanimated 在变换中使用 '45deg' 字符串 → EaseView 使用 45(数字,单位为度)。去掉 'deg' 后缀并解析为数字。
  • 平移: 两者都使用 DIP(与密度无关的像素)。无需转换。
  • 缩放: 两者都使用无单位的乘数。无需转换。
  • 弹簧速度: 单位与所动画的属性相同,按秒计——translate 使用 DIP,rotate 使用度,scale 和 opacity 使用无单位数值。无需转换,但请注意,速度是按转换设置的,因此,translate 和 scale 共用的 transform 转换只能携带一个有意义的速度值。如果两者都需要速度值,请拆分这些类别。

阶段 3:试运行报告

在要求用户选择组件之前,务必先打印此报告。用户必须在阶段 4 之前看到此报告。

打印一份结构化报告。暂时不要应用任何更改。

格式:

## Migration Report

### Summary
- Files scanned: X
- Components with animations: Y
- Migratable: Z  |  Not migratable: W

### Migratable Components

#### `path/to/file.tsx` — ComponentName
**Current:** Brief description of what the animation does and which API it uses
**Proposed:** What the EaseView equivalent looks like (include exact transition values with mapped defaults)
**Changes:** What will be added/removed/modified
**Note:** (only if applicable) "Requires state changes for exit animation" or other caveats

### Not Migratable (will be skipped)

#### `path/to/file.tsx` — ComponentName
**Reason:** Why it can't be migrated (from decision tree)

此报告必须作为对话中的文本输出打印,而不能放在计划中或折叠起来。用户需要在阶段 4 选择组件之前阅读报告。


阶段 4:用户确认

重要:此处必须使用 AskUserQuestion 工具。不要使用计划模式,不要以文本提示,不要在对话中直接提问。请直接调用 AskUserQuestion 工具。

使用以下确切参数调用 AskUserQuestion:

  • multiSelect:true
  • questions:包含一个问题对象,字段如下:
    • header:"Migrate"
    • question:"Which components should be migrated to EaseView? All are selected — deselect any to skip."
    • multiSelect:true
    • options:每个可迁移组件对应一个条目,每个条目包含:
      • label:组件名称(例如,"AnimatedButton")
      • description:文件路径和简要动画说明(例如,"src/components/animated-button.tsx — spring scale on press")

包含 2 个可迁移组件的工具调用示例:

{
  "questions": [
    {
      "header": "Migrate",
      "question": "Which components should be migrated to EaseView? All are selected — deselect any to skip.",
      "multiSelect": true,
      "options": [
        {
          "label": "AnimatedButton",
          "description": "src/components/simple/animated-button.tsx — spring scale on press"
        },
        {
          "label": "Collapsible",
          "description": "src/components/ui/collapsible.tsx — fade-in entering animation"
        }
      ]
    }
  ]
}

**等待用户回复后再继续。**不要进入计划模式。未经用户选择组件,不要应用任何更改。

如果用户未选择任何内容,或选择“Other”取消,则中止并显示:"Migration aborted. No changes were made."

只有用户确认的组件才能继续进入阶段 5。


阶段 5:应用迁移

对每个已确认的组件,应用迁移:

迁移步骤(针对每个组件)
  1. 如果尚未添加,则导入 EaseView:

    import { EaseView } from 'react-native-ease';
    

1b. 如果 usesNativeWind 为 true,检查项目中是否已存在 import 'react-native-ease/nativewind'(搜索所有文件)。如果不存在,请将其添加到应用的根入口文件中(例如 _layout.tsx、App.tsx 或 index.tsx,以最早的入口为准)。所有迁移只需添加一次,无需为每个组件重复添加。

  1. 替换动画视图:

    • Animated.View → EaseView
    • <Animated.View style={[styles.box, animatedStyle]}> → <EaseView style={styles.box} animate={{ ... }} transition={{ ... }}>
  2. 将动画钩子转换为属性:

    • 移除 useSharedValue、useAnimatedStyle、withTiming、withSpring、withRepeat 调用
    • 将它们的值转换为 animate、initialAnimate 和 transition 属性
  3. 转换进入/退出动画:

    • entering={FadeIn} → 在 EaseView 上设置 initialAnimate={{ opacity: 0 }},并设置 animate={{ opacity: 1 }}

    • 对于 exiting:引入一个状态变量和 onTransitionEnd 回调:

      const [visible, setVisible] = useState(true);
      const [mounted, setMounted] = useState(true);
      
      // 触发退出时:
      setVisible(false);
      
      // 在 EaseView 上:
      {
        mounted && (
          <EaseView
            animate={{ opacity: visible ? 1 : 0 }}
            transition={{ type: 'timing', duration: 300 }}
            onTransitionEnd={({ finished }) => {
              if (finished && !visible) setMounted(false);
            }}
          >
            ...
          </EaseView>
        );
      }
      
  4. 清理导入:

    • 移除文件中不再使用的 Reanimated 导入
    • 保留同一文件中仍被未迁移代码引用的 Reanimated 导入
    • 切勿移除仍在使用的导入
  5. 输出进度:

    [1/N] Migrated ComponentName in path/to/file.tsx
    
安全规则

这些规则不可妥协。违反规则会破坏用户代码。

  1. 不确定时,跳过。 如果某种模式含义不明,或你对迁移没有把握,请将其加入“不可迁移”列表,并注明原因:“复杂模式 — 建议人工审核”
  2. 绝不移除文件中其他位置仍在使用的导入。 移除动画代码后,请检查所有剩余代码行,确认其中是否引用了要移除的每个导入。
  3. 保留所有非动画逻辑。 事件处理程序、状态管理、效果、回调——除非与正在迁移的动画直接相关,否则一律不要改动。
  4. 保留组件结构和公共 API。 属性、ref 转发、导出类型——保持不变。
  5. 正确处理混合文件。 如果文件中同时包含可迁移和不可迁移的动画,只迁移安全的部分。如果仍有 Reanimated 代码,则保留 Reanimated 导入。
  6. 正确映射旋转单位。 Reanimated 的 '45deg' 字符串 → EaseView 的 45 数值。如果源代码使用弧度,请转换:radians * (180 / Math.PI)。
  7. 正确映射缓动预设。 请参阅阶段 2 中的映射表。
  8. 不要引入 TypeScript 错误。 确保迁移后的所有类型都正确。如果原始代码使用带类型的共享值,请确保 EaseView 属性与之匹配。

阶段 6:最终报告

应用所有迁移后,输出:

## Migration Complete

### Changed (X components)
- `path/to/file.tsx` — ComponentName: brief description of what was migrated

### Unchanged (Y components)
- `path/to/file.tsx` — ComponentName: reason skipped

### Next Steps
- Run your app and verify animations visually
- Run your test suite to check for regressions
- If no Reanimated code remains, consider removing `react-native-reanimated` from dependencies

EaseView API 参考(确保迁移准确)

支持动画的属性

animate prop 中的所有属性:

属性类型默认值说明
opacitynumber1范围为 0–1
translateXnumber0单位为 DIP(与密度无关的像素)
translateYnumber0单位为 DIP
scalenumber1scaleX + scaleY 的简写
scaleXnumber1覆盖 X 轴的 scale 值
scaleYnumber1覆盖 Y 轴的 scale 值
rotatenumber0绕 Z 轴旋转,单位为度
rotateXnumber0绕 X 轴旋转,单位为度(3D)
rotateYnumber0绕 Y 轴旋转,单位为度(3D)
borderRadiusnumber0单位为像素
backgroundColorColorValue'transparent'任意 RN 颜色值
borderWidthnumber0单位为像素
borderColorColorValue'black'任意 RN 颜色值
shadowOpacitynumber0范围为 0–1(仅限 iOS)
shadowRadiusnumber0单位为像素(仅限 iOS)
shadowColorColorValue'black'任意 RN 颜色值(仅限 iOS)
shadowOffsetobject{width:0,height:0}{ width, height }(仅限 iOS)
elevationnumber0Android 材质阴影
过渡类型

定时:

transition={{
  type: 'timing',
  duration: 300,        // ms, default 300
  easing: 'easeInOut',  // 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | [x1,y1,x2,y2]
  delay: 0,             // ms, default 0
  loop: 'repeat',       // 'repeat' | 'reverse' — requires initialAnimate
}}

弹簧:

transition={{
  type: 'spring',
  damping: 15,      // default 15
  stiffness: 120,   // default 120
  mass: 1,          // default 1
  velocity: 0,      // property units/s, default 0, native only
  delay: 0,         // ms, default 0
}}

无(即时):

transition={{ type: 'none' }}
关键 Props
  • animate — 动画属性的目标值
  • initialAnimate — 起始值(挂载时动画过渡到 animate)
  • transition — 动画配置:单个 SingleTransition(timing/spring/none),或带分类键(default、transform、opacity、borderRadius、backgroundColor、border、shadow)的 TransitionMap
  • onTransitionEnd — 回调,参数为 { finished: boolean }
  • paused — 暂停正在运行的动画(包括循环动画)并保持当前状态;设为 false 后从同一位置继续
  • onPaused — 回调,参数为 { values },即 paused 生效时屏幕上显示的 animate 键值
  • transformOrigin — 枢轴点,格式为 { x: 0-1, y: 0-1 },默认位于中心
  • useHardwareLayer — Android GPU 优化(布尔值,默认值为 false)
  • className — NativeWind / Tailwind CSS 类字符串(项目中需要使用 NativeWind)
重要限制
  • 循环动画需要使用 timing(不能使用 spring),且 initialAnimate 必须定义起始值
  • 支持按属性配置过渡 — 传入带分类键(default、transform、opacity、borderRadius、backgroundColor、border、shadow)的 TransitionMap,即可为不同属性组使用不同配置
  • 不支持动画序列 — 没有与 withSequence 等效的功能。支持通过 delay 过渡 prop 实现简单的 withDelay
  • 不支持由手势或滚动驱动的动画 — EaseView 仅由状态驱动
  • style/animate 冲突 — 如果某个属性同时出现在 style 和 animate 中,则以动画值为准

评论

支持 Markdown