HarmonyOS APP开发---"心动卡"社交匹配App,需要用到这个库
做一个社交匹配 App,用户左右滑卡片决定喜欢/跳过,滑出的卡片要有弹性回弹或飞出动画,下一张卡片从后方升起。Animated API跑在JS线程,滑多了就卡------
@react-native-ohos/react-native-reanimated在 UI 线程跑动画,配合手势零延迟跟手。
📦 仓库地址:gitcode.com/CPF-RN/rntp... | 安装:npm install @react-native-ohos/react-native-reanimated
写在前面
"心动卡"的核心交互是"Tinder 式卡片滑动":
- 用户右滑 = 喜欢,卡片向右飞出 + 淡出
- 用户左滑 = 跳过,卡片向左飞出 + 淡出
- 滑动不够远 = 弹簧回弹原位
- 卡片飞出后,下一张从后方升起 + 缩放进入
- 滑动时卡片有旋转角度 + 透明度渐变 + 背景卡片放大
这些动画的痛点:
- 手势跟手:拖拽卡片必须零延迟,慢半拍用户立刻能感觉到
- 连续动画:弹簧回弹 + 飞出 + 下一张升起,多个动画串联
- JS 线程忙:卡片里可能有图片在解码,JS 线程一忙动画就掉帧
react-native-reanimated 把动画计算移到 UI 线程 (原生层),用 SharedValue + worklet 不走 JS Bridge------60fps 丝滑,手势零延迟。
这篇文章聊什么
- 卡片拖拽------Pan 手势 + SharedValue 驱动位移旋转
- 弹簧回弹 vs 飞出------判断滑动距离决定动画
- 下一张卡片升起------进入动画 + 堆叠效果
flowchart TD
A[用户拖拽卡片] --> B[Gesture.Pan onUpdate]
B --> C[SharedValue: translateX/rotate/scale]
C --> D[UI线程实时渲染]
D --> E{手指抬起}
E -->|滑动> 阈值| F[withTiming 飞出]
E -->|滑动< 阈值| G[withSpring 回弹]
F --> H[触发 like/pass 回调]
H --> I[下一张卡片 entering 动画]
G --> J[回到原位]
第一步:安装
bash
npm install @react-native-ohos/react-native-reanimated react-native-gesture-handler
babel 配置:
js
plugins: ['react-native-reanimated/plugin']
第二步:卡片拖拽 + 旋转
用 Pan 手势驱动卡片的位移、旋转和透明度:
tsx
import { GestureDetector, Gesture } from 'react-native-gesture-handler'
import Animated, {
useSharedValue, useAnimatedStyle, withSpring, withTiming,
runOnJS, interpolate, Extrapolate,
} from 'react-native-reanimated'
const SWIPE_THRESHOLD = 120 // 滑动超过 120px 触发飞出
function SwipeableCard({ profile, onSwipe, index }: {
profile: Profile
onSwipe: (direction: 'left' | 'right') => void
index: number
}) {
const translateX = useSharedValue(0)
const translateY = useSharedValue(0)
const pan = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX
translateY.value = e.translationY
})
.onEnd((e) => {
if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
// 飞出:向滑动方向加速飞出
const direction = translateX.value > 0 ? 1 : -1
translateX.value = withTiming(direction * 500, { duration: 300 }, () => {
runOnJS(onSwipe)(direction > 0 ? 'right' : 'left')
})
} else {
// 回弹:弹簧回到原位
translateX.value = withSpring(0, { damping: 15, stiffness: 150 })
translateY.value = withSpring(0, { damping: 15, stiffness: 150 })
}
})
// 动画样式:位移 + 旋转 + 透明度渐变
const animatedStyle = useAnimatedStyle(() => {
const rotate = interpolate(
translateX.value,
[-200, 0, 200],
[-15, 0, 15], // 最多旋转 15 度
Extrapolate.CLAMP,
)
const opacity = interpolate(
Math.abs(translateX.value),
[0, 200],
[1, 0.5], // 滑得越远越透明
Extrapolate.CLAMP,
)
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ rotate: `${rotate}deg` },
],
opacity,
}
})
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, animatedStyle]}>
{/* 卡片内容:头像、昵称、简介 */}
</Animated.View>
</GestureDetector>
)
}
关键点:
onUpdate实时更新 SharedValue------手势在原生层识别,动画在 UI 线程执行,零延迟跟手rotate随位移插值------卡片拖拽时有"翻牌"的感觉opacity随位移渐变------滑得越远越透明,给用户"快飞出了"的视觉反馈
第三步:飞出 vs 回弹
手指抬起时判断滑动距离:
tsx
.onEnd((e) => {
if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
// ===== 飞出 =====
const direction = translateX.value > 0 ? 1 : -1
translateX.value = withTiming(
direction * 500, // 向滑动方向飞出 500px
{ duration: 300 },
() => {
// 动画完成后回调 JS 线程
runOnJS(onSwipe)(direction > 0 ? 'right' : 'left')
},
)
} else {
// ===== 回弹 =====
translateX.value = withSpring(0, {
damping: 15, // 适当阻尼,弹得不太多
stiffness: 150, // 弹簧刚度,回弹速度
})
}
})
- 飞出 :
withTiming线性加速飞出,300ms 完成 - 回弹 :
withSpring弹簧回弹,有物理感的弹动
全程在 UI 线程执行,JS 线程哪怕正在解码下一张卡片的图片,动画也丝滑不掉帧。
第四步:下一张卡片升起(堆叠效果)
多张卡片堆叠,后面的卡片随前一张的滑动距离放大上移:
tsx
function CardStack({ profiles }: { profiles: Profile[] }) {
const [currentIndex, setCurrentIndex] = useState(0)
return (
<View style={{ flex: 1, position: 'relative' }}>
{/* 渲染当前卡片 + 后面 2 张(堆叠效果) */}
{profiles.slice(currentIndex, currentIndex + 3).reverse().map((profile, i) => {
const stackIndex = 2 - i // 0=当前, 1=下一张, 2=再下一张
return (
<BackgroundCard
key={profile.id}
profile={profile}
stackIndex={stackIndex}
/>
)
})}
{/* 最上层是可滑动的卡片 */}
{profiles[currentIndex] && (
<SwipeableCard
profile={profiles[currentIndex]}
onSwipe={() => setCurrentIndex((i) => i + 1)}
index={currentIndex}
/>
)}
</View>
)
}
// 后面的卡片随层级缩放 + 上移
function BackgroundCard({ profile, stackIndex }: { profile: Profile; stackIndex: number }) {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: 1 - stackIndex * 0.05 }, // 后面的卡片小 5%
{ translateY: stackIndex * -10 }, // 后面的卡片上移 10px
],
opacity: stackIndex === 0 ? 1 : 0.9,
zIndex: 10 - stackIndex,
}))
return <Animated.View style={[styles.card, animatedStyle]}>{/* ... */}</Animated.View>
}
为什么"心动卡"选了 Reanimated?
| 需求 | RN Animated | @react-native-ohos/react-native-reanimated |
|---|---|---|
| 手势跟手 | JS 线程,延迟 | ✅ UI 线程,零延迟 |
| JS 忙时 | ❌ 掉帧 | ✅ 不受影响 |
| 弹簧动画 | ✅ 但 JS 线程 | ✅ UI 线程弹簧 |
| 插值旋转 | ✅ | ✅ interpolate |
| 连续动画 | ❌ 难串联 | ✅ withSequence |
| 声明式进入 | ❌ | ✅ entering |
总结
"心动卡"这个场景里,@react-native-ohos/react-native-reanimated 解决了三件事:
- 零延迟跟手------Pan 手势 + SharedValue 在 UI 线程驱动,拖拽像素级跟手
- 飞出/回弹 ------
withTiming飞出 +withSpring回弹,物理感动画 - 堆叠效果------后面卡片随层级缩放上移,进入动画丝滑
如果你也在做卡片滑动、侧滑删除、拖拽排序等手势驱动的动画场景,react-native-reanimated 是 RN 鸿蒙动画性能的天花板。