关于 react-native-ease-refactor
react-native-ease refactor 是迁移 Skill,可将受支持的 Reanimated 和 React Native Animated 代码转换为 react-native-ease 的 EaseView 组件。它会扫描动画模式并报告可迁移项,修改前需确认。
主要功能
- 01扫描 Reanimated 和 Animated 用法
- 02分类可迁移的动画模式
- 03映射时序、弹簧和缓动设置
- 04报告不支持的模式及原因
- 05修改前要求选择组件
适用场景
- 01将状态驱动的动画迁移到 EaseView
- 02选择迁移范围前检查动画代码
- 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:发现
扫描用户的项目,查找动画代码:
-
使用 Grep 检测项目是否使用 NativeWind:
- 模式:在
**/*.{ts,tsx,js,jsx}中搜索from ['"]nativewind['"] - 同时检查
package.json的依赖中是否包含"nativewind" - 如果检测到 NativeWind,请设置标志
usesNativeWind = true,供阶段 5 使用
- 模式:在
-
检测 Reanimated 版本(阶段 2 中映射默认值时需要):
- 读取
package.json,检查dependencies或devDependencies中的react-native-reanimated版本 - 如果版本为
^4或>=4.0.0,设置reanimatedVersion = 4 - 否则设置
reanimatedVersion = 3(涵盖默认值相同的 v2/v3)
- 读取
-
使用 Grep 查找所有从
react-native-reanimated导入内容的文件:- 模式:
from ['"]react-native-reanimated['"] - 在
**/*.{ts,tsx,js,jsx}中搜索
- 模式:
-
使用 Grep 查找所有使用 React Native 内置
AnimatedAPI 的文件:- 模式:
from ['"]react-native['"],且同时使用Animated - 模式:
Animated\.View|Animated\.Text|Animated\.Image|Animated\.Value|Animated\.timing|Animated\.spring
- 模式:
-
使用 Grep 查找已使用
react-native-ease的文件(以避免重复迁移):- 模式:
from ['"]react-native-ease['"]
- 模式:
-
读取每个包含动画代码的文件。整理出组件及其动画模式列表。
扫描时排除:
node_modules/*.test.*和*.spec.*文件- 构建输出目录(
lib/、build/、dist/)
阶段 2:分类
将找到的每个组件分类为可迁移或不可迁移。
决策树
按顺序应用以下检查。第一个匹配项决定结果:
- 使用手势 API?(
Gesture.Pan、Gesture.Pinch、Gesture.Rotation、useAnimatedGestureHandler)→ 不可迁移 — “手势驱动的动画” - 使用滚动处理程序?(
useAnimatedScrollHandler、带有Animated.event的onScroll)→ 不可迁移 — “滚动驱动的动画” - 使用共享元素过渡?(
sharedTransitionTag)→ 不可迁移 — “共享元素过渡” - 使用
runOnUI或 worklet 指令?→ 不可迁移 — “需要 worklet 运行时” - 使用
withSequence?→ 不可迁移 — “不支持动画序列” 5b. 使用withDelay包装单个动画(withTiming/withSpring)?→ 可迁移 — 将其映射为过渡上的delay5c. 使用withDelay包装withSequence或嵌套的withDelay?→ 不可迁移 — “不支持复杂延迟/序列” - 使用复杂的
interpolate()?(输入/输出值超过 2 个)→ 不可迁移 — “复杂插值” - 使用
layout={...}属性?→ 不可迁移 — “布局动画” - 对不支持的属性进行动画处理?(除以下属性之外的任何属性:opacity、translateX、translateY、scale、scaleX、scaleY、rotate、rotateX、rotateY、borderRadius、backgroundColor、borderWidth、borderColor、shadowOpacity、shadowRadius、shadowColor、shadowOffset、elevation)→ 不可迁移 — “动画处理了不支持的属性:
<prop>” - 不同属性使用不同的过渡配置?(例如,opacity 使用 200ms timing,scale 使用 spring)→ 可迁移 — 将其映射为使用类别键(
transform、opacity、borderRadius、backgroundColor、border、shadow、default)的TransitionMap - 不是由状态驱动?(动画由手势/滚动值触发,而非 React 状态)→ 不可迁移 — “非状态驱动”
- 其他情况→ 可迁移
可迁移模式映射
使用下表将 Reanimated/Animated 模式转换为 EaseView:
| Reanimated / Animated 模式 | EaseView 对应方式 |
|---|---|
useSharedValue + useAnimatedStyle + withTiming 用于 opacity、translate、scale、rotate、borderRadius、backgroundColor | animate={{ prop: value }} + transition={{ type: 'timing', duration, easing }} |
withSpring | transition={{ type: 'spring', damping, stiffness, mass, velocity }} |
entering={FadeIn} / FadeIn.duration(N) | initialAnimate={{ opacity: 0 }} + animate={{ opacity: 1 }} + timing 过渡 |
entering={FadeInDown} / FadeInUp | initialAnimate={{ opacity: 0, translateY: ±value }} + animate={{ opacity: 1, translateY: 0 }} |
entering={SlideInLeft} / SlideInRight | initialAnimate={{ translateX: ±value }} + animate={{ translateX: 0 }} |
entering={SlideInUp} / SlideInDown | initialAnimate={{ 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.linear | easing: '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.spring | animate + 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/withSpring | transition={{ opacity: { type: 'timing', ... }, transform: { type: 'spring', ... } }}(按属性映射) |
默认值映射
重要:Reanimated 和 EaseView 的默认值不同。你必须显式设置值,才能保留原有动画行为。不要假设 EaseView 的默认值与 Reanimated 的默认值一致。
使用第 1 阶段的 reanimatedVersion 选择正确的默认值。
withSpring → EaseView 弹簧动画
Reanimated v2/v3 默认值:
| 参数 | Reanimated v2/v3 | EaseView 默认值 | 操作 |
|---|---|---|---|
damping | 10 | 15 | 必须设置 damping: 10 |
stiffness | 100 | 120 | 必须设置 stiffness: 100 |
mass | 1 | 1 | 相同 — 省略 |
velocity | 0 | 0 | 相同 — 仅在设置时沿用 |
Reanimated v4 默认值:
| 参数 | Reanimated v4 | EaseView 默认值 | 操作 |
|---|---|---|---|
damping | 120 | 15 | 必须设置 damping: 120 |
stiffness | 900 | 120 | 必须设置 stiffness: 900 |
mass | 4 | 1 | 必须设置 mass: 4 |
velocity | 0 | 0 | 相同 — 仅在设置时沿用 |
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 默认值 | 操作 |
|---|---|---|---|
duration | 300 | 300 | 相同 — 省略 |
easing | Easing.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 默认值 | 操作 |
|---|---|---|---|
duration | 500 | 300 | 必须设置 duration: 500 |
easing | Easing.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) | 40 | 120 | 必须设置 stiffness: 40 |
| damping(friction) | 7 | 15 | 必须设置 damping: 7 |
| mass | 1 | 1 | 相同 — 省略 |
| velocity | 0 | 0 | 相同 — 仅在设置时沿用 |
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:truequestions:包含一个问题对象,字段如下:header:"Migrate"question:"Which components should be migrated to EaseView? All are selected — deselect any to skip."multiSelect:trueoptions:每个可迁移组件对应一个条目,每个条目包含: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:应用迁移
对每个已确认的组件,应用迁移:
迁移步骤(针对每个组件)
-
如果尚未添加,则导入 EaseView:
import { EaseView } from 'react-native-ease';
1b. 如果 usesNativeWind 为 true,检查项目中是否已存在 import 'react-native-ease/nativewind'(搜索所有文件)。如果不存在,请将其添加到应用的根入口文件中(例如 _layout.tsx、App.tsx 或 index.tsx,以最早的入口为准)。所有迁移只需添加一次,无需为每个组件重复添加。
-
替换动画视图:
Animated.View→EaseView<Animated.View style={[styles.box, animatedStyle]}>→<EaseView style={styles.box} animate={{ ... }} transition={{ ... }}>
-
将动画钩子转换为属性:
- 移除
useSharedValue、useAnimatedStyle、withTiming、withSpring、withRepeat调用 - 将它们的值转换为
animate、initialAnimate和transition属性
- 移除
-
转换进入/退出动画:
-
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> ); }
-
-
清理导入:
- 移除文件中不再使用的 Reanimated 导入
- 保留同一文件中仍被未迁移代码引用的 Reanimated 导入
- 切勿移除仍在使用的导入
-
输出进度:
[1/N] Migrated ComponentName in path/to/file.tsx
安全规则
这些规则不可妥协。违反规则会破坏用户代码。
- 不确定时,跳过。 如果某种模式含义不明,或你对迁移没有把握,请将其加入“不可迁移”列表,并注明原因:“复杂模式 — 建议人工审核”
- 绝不移除文件中其他位置仍在使用的导入。 移除动画代码后,请检查所有剩余代码行,确认其中是否引用了要移除的每个导入。
- 保留所有非动画逻辑。 事件处理程序、状态管理、效果、回调——除非与正在迁移的动画直接相关,否则一律不要改动。
- 保留组件结构和公共 API。 属性、ref 转发、导出类型——保持不变。
- 正确处理混合文件。 如果文件中同时包含可迁移和不可迁移的动画,只迁移安全的部分。如果仍有 Reanimated 代码,则保留 Reanimated 导入。
- 正确映射旋转单位。 Reanimated 的
'45deg'字符串 → EaseView 的45数值。如果源代码使用弧度,请转换:radians * (180 / Math.PI)。 - 正确映射缓动预设。 请参阅阶段 2 中的映射表。
- 不要引入 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 中的所有属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
opacity | number | 1 | 范围为 0–1 |
translateX | number | 0 | 单位为 DIP(与密度无关的像素) |
translateY | number | 0 | 单位为 DIP |
scale | number | 1 | scaleX + scaleY 的简写 |
scaleX | number | 1 | 覆盖 X 轴的 scale 值 |
scaleY | number | 1 | 覆盖 Y 轴的 scale 值 |
rotate | number | 0 | 绕 Z 轴旋转,单位为度 |
rotateX | number | 0 | 绕 X 轴旋转,单位为度(3D) |
rotateY | number | 0 | 绕 Y 轴旋转,单位为度(3D) |
borderRadius | number | 0 | 单位为像素 |
backgroundColor | ColorValue | 'transparent' | 任意 RN 颜色值 |
borderWidth | number | 0 | 单位为像素 |
borderColor | ColorValue | 'black' | 任意 RN 颜色值 |
shadowOpacity | number | 0 | 范围为 0–1(仅限 iOS) |
shadowRadius | number | 0 | 单位为像素(仅限 iOS) |
shadowColor | ColorValue | 'black' | 任意 RN 颜色值(仅限 iOS) |
shadowOffset | object | {width:0,height:0} | { width, height }(仅限 iOS) |
elevation | number | 0 | Android 材质阴影 |
过渡类型
定时:
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)的TransitionMaponTransitionEnd— 回调,参数为{ 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中,则以动画值为准