在 React Native 中创建更加符合物理手感的卡片划走手势,告别不跟手!
#移动端开发在 React Native 开发中,初学开发者在处理卡片划走效果时,可能会只依赖一个简单的距离阈值(Threshold),比如:if (distance > 阈值) swipe();。
但在真实世界里,我们丢出一个物体,即使位移还没过半,只要速度够快,它也应该飞出去。同理,如果位置过半了,但是用户给卡片一个返回的速度,卡片也应该回到原位。
也就是说,一个“自然”的手势交互不仅要看位置(Translation),更要看速度(Velocity)。
1. 为什么只看距离会不跟手?
只看位移会导致:快速滑动时卡片像粘在手上,必须硬生生拽过临界点,这会让用户觉得“不跟手”。
2. 优化手感
我们要做的,是预测用户的“意图”。通过将当前位置和瞬时速度结合,计算出卡片在松手后的一小段时间内本该到达的位置。
考虑惯性权重
在代码实现中,将添加 20% 的惯性权重 的位移绝对值成为 “投影位移”。在之后的划走/回弹判断中,我们将会以投影位移为判断标准:
const projectedX = Math.abs(translateX.value) + Math.abs(velocityX) * 0.2;
const projectedY = Math.abs(translateY.value) + Math.abs(velocityY) * 0.2;
考虑反向阻断
想象一下,如果你向右拉到边缘,但松手那一刻速度是向左的(即使位移 translateX.value 还在右边),这说明用户想“撤回”操作。此时不应甩出,而应触发回弹。
// 投影位移小于阈值 或 速度方向与位移方向不同时,回弹
if ((projectedX < THRESHOLD || translateX.value > 0 !== velocityX > 0)
&& (projectedY < THRESHOLD || translateY.value > 0 !== velocityY > 0)) {
// 回弹逻辑
translateX.value = withSpring(0);
translateY.value = withSpring(0);
return;
}
3. 完善更多…
除优化手感外,还需要实现一些额外的逻辑才能实现图片中的卡片 Swipe 效果:
锁定滑动方向
const panGesture = Gesture.Pan()
.onUpdate((e) => {
let verticalSwiping = Math.abs(e.translationY) > Math.abs(e.translationX);
if (verticalSwiping) {
translateX.value = 0;
translateY.value = e.translationY;
} else {
translateY.value = 0;
translateX.value = e.translationX;
}
})
判断划出方向
(预先传入 onSwipeComplete 函数)
.onEnd((e) => {
// ... 省略上文中的回弹逻辑
// 执行甩出
if (projectedX > projectedY) {
// 甩出方向以速度方向为准
const direction = velocityX > 0 ? 'right' : 'left';
translateX.value = withTiming(direction === 'right' ? width : -width, { duration: 200 }, (finished) => {
if (finished) runOnJS(onSwipeComplete)(direction);
});
} else {
// 甩出方向以速度方向为准
const direction = velocityY > 0 ? 'down' : 'up';
translateY.value = withTiming(direction === 'down' ? height : -height, { duration: 200 }, (finished) => {
if (finished) runOnJS(onSwipeComplete)(direction);
});
}
})