ゲーム開発部 (⸝⸝ >ヮ<) !

在 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);
        });
      }
    })

组件源码…

SwipeAwayCard_Demo.tsx