如何用React Native Reanimated或Skia实现图形沿路径带动画移动?
React Native Skia实现红球沿路径弹簧动画移动的正确方法
如何让红球沿黑色路径移动,并应用React Native Reanimated的
withSpring等动画效果?
我尝试用React Native Skia编写如下代码:
const progress = useSharedValue(0); const geo = new PathGeometry(path); const totalLength = geo.getTotalLength(); const transform = useDerivedValue(() => { const currentLen = interpolate(progress.value, [0, 1], [0, totalLength]); const pos = geo.getPointAtLength(currentLen); return translate({x:pos.x,y:pos.y}); }); const handlePress = () => { progress.value = withSpring(1); };
但收到错误提示:
ReanimatedError: [Reanimated] Trying to access property getPointAtLength of an object which cannot be sent to the UI runtime. js engine: reanimated
之后我改用如下代码实现:
const geo = new PathGeometry(path); const totalLength = geo.getTotalLength(); const pos = geo.getPointAtLength(0); const posX = useSharedValue(pos.x); const posY = useSharedValue(pos.y); let i = 0; setInterval(() => { const nPos = geo.getPointAtLength(i); posX.value = nPos.x; posY.value = nPos.y; i++; }, 16);
该代码虽能实现移动但效果生硬,且无法应用withSpring这类动画效果。
问题原因
第一个方案报错是因为PathGeometry属于JS线程对象,无法直接在Reanimated的UI线程(useDerivedValue运行环境)中调用其方法。第二个方案用setInterval逐帧更新,本质是手动模拟动画帧,无法和Reanimated的弹簧动画逻辑结合,所以效果生硬。
正确实现思路
要在Reanimated的UI线程中使用路径信息,需使用Reanimated兼容的PathMeasure工具类,它支持在UI线程中直接获取路径长度、指定位置坐标等信息,完美适配withSpring这类动画API。
完整代码示例
import React from 'react'; import { View, TouchableOpacity, Text } from 'react-native'; import { Canvas, Path, Circle } from '@shopify/react-native-skia'; import { useSharedValue, useDerivedValue, withSpring, interpolate, translate } from 'react-native-reanimated'; const PathAnimation = () => { // 替换为你自己的目标路径 const path = `M 50 50 C 150 50, 150 150, 250 150 C 350 150, 350 50, 450 50`; // 控制动画进度的共享值(0表示起点,1表示终点) const progress = useSharedValue(0); // 在UI线程中初始化PathMeasure,绑定目标路径 const pathMeasure = useDerivedValue(() => { const measure = new PathMeasure(); measure.setPath(path); return measure; }, [path]); // 根据进度计算红球的位置变换 const transform = useDerivedValue(() => { const measure = pathMeasure.value; const totalLength = measure.getLength(); // 插值计算当前进度对应的路径长度 const currentLength = interpolate(progress.value, [0, 1], [0, totalLength]); // 获取当前长度对应的坐标点 const point = measure.getPosTan(currentLength); // 平移红球到目标点,减去半径让球心对准路径点 return translate({ x: point.x - 10, y: point.y - 10 }); }, [pathMeasure]); // 触发弹簧动画,点击后在起点/终点间切换 const handlePress = () => { progress.value = withSpring(progress.value === 1 ? 0 : 1, { damping: 10, stiffness: 100, }); }; return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <TouchableOpacity onPress={handlePress} style={{ marginBottom: 20, padding: 12, backgroundColor: '#2196F3', borderRadius: 6 }} > <Text style={{ color: 'white', fontSize: 16 }}>触发动画</Text> </TouchableOpacity> <Canvas style={{ width: 500, height: 200 }}> {/* 绘制黑色路径 */} <Path path={path} strokeWidth={2} color="black" /> {/* 绘制红球并应用位置变换 */} <Circle cx={0} cy={0} r={10} color="red" transform={transform} /> </Canvas> </View> ); }; export default PathAnimation;
关键说明
PathMeasure适配性:它是Reanimated官方支持的工具类,可直接在UI线程的useDerivedValue中使用,避免跨线程访问错误。getPosTan方法:该方法能获取路径上指定长度位置的坐标点和切线方向,这里仅用坐标点实现平移。- 弹簧动画结合:通过
progress共享值控制整体动画进度,直接用withSpring更新progress,就能让红球沿路径自然呈现弹簧运动效果。 - 坐标修正:将红球的圆心设为(0,0),通过
translate移动到路径点时减去球的半径,确保球心与路径点重合,避免视觉偏移。
内容的提问来源于stack exchange,提问作者YiSong
相关产品推荐
相关产品推荐

